A .NET program or library, an assembly, is a PE file whose code is CIL bytecode for
the .NET runtime rather than machine code. Data directory 14 points at its CLR runtime
header, which locates the metadata: the tables of types, methods and fields, and the
heaps of names, strings and signatures they use. The format is standardised in ECMA-335.
An assembly compiled for any processor (“Any CPU”) is a PE32 file with machine
0x014C and the ILONLY flag, and the runtime runs it as 64-bit code on a
64-bit system unless it is marked 32-bit. Its only x86 code is a stub at the entry
point that jumps to _CorExeMain or _CorDllMain in
mscoree.dll, its one import.
The CLR runtime header
IMAGE_COR20_HEADER, 72 bytes. Each “directory” is an RVA and a size, 8 bytes.
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | cb | The size of the header, 72. |
| 4 | 2 | u16 | MajorRuntimeVersion | 2, for every runtime since .NET 2.0. |
| 6 | 2 | u16 | MinorRuntimeVersion | 5; 0 in .NET 1.x files. |
| 8 | 8 | directory | MetaData | The metadata root. |
| 16 | 4 | u32 | Flags | See Flags. |
| 20 | 4 | u32 | EntryPointToken | The metadata token of the entry method (0x06…, a MethodDef) or of a file (0x26…); with NATIVE_ENTRYPOINT, the RVA of native code; 0 for a library. |
| 24 | 8 | directory | Resources | The managed resources. |
| 32 | 8 | directory | StrongNameSignature | The strong name signature. |
| 40 | 8 | directory | CodeManagerTable | Unused, 0. |
| 48 | 8 | directory | VTableFixups | The VTable fixups. |
| 56 | 8 | directory | ExportAddressTableJumps | Unused, 0. |
| 64 | 8 | directory | ManagedNativeHeader | Precompiled native code: a ReadyToRun header, which starts RTR, or 0. |
00000200 10 9A 17 00 00 00 00 00 48 00 00 00 02 00 05 00
00000210 58 B5 05 00 88 E4 11 00 01 00 00 00 00 00 00 00
0x208: 72 bytes, runtime 2.5, metadata at RVA 0x5B558 (1,172,616 bytes), flags ILONLY, no entry pointFlags
| Bit | Name | Meaning |
|---|---|---|
0x00000001 | ILONLY | The image holds only IL code, no native code. |
0x00000002 | 32BITREQUIRED | Run only as a 32-bit process. |
0x00000004 | IL_LIBRARY | An IL library. |
0x00000008 | STRONGNAMESIGNED | The image has a strong name signature. |
0x00000010 | NATIVE_ENTRYPOINT | EntryPointToken is the RVA of native code (C++/CLI). |
0x00010000 | TRACKDEBUGDATA | Track debug data. |
0x00020000 | 32BITPREFERRED | With 32BITREQUIRED: prefer 32-bit, but run anywhere (“Any CPU, 32-bit preferred”). |
The metadata root
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | Signature | 0x424A5342, the bytes BSJB. |
| 4 | 2 | u16 | MajorVersion | 1. |
| 6 | 2 | u16 | MinorVersion | 1. |
| 8 | 4 | u32 | Reserved | 0. |
| 12 | 4 | u32 | Length | The length of the version string, padded to a multiple of 4. |
| 16 | Length | char[] | Version | The runtime version, such as v2.0.50727 or v4.0.30319, zero-padded. |
| 16 + Length | 2 | u16 | Flags | 0. |
| 18 + Length | 2 | u16 | Streams | The number of stream headers that follow. |
42 53 4A 42 01 00 01 00 00 00 00 00 0C 00 00 00 BSJB, version 1.1, length 12
76 32 2E 30 2E 35 30 37 32 37 00 00 "v2.0.50727" and padding
00 00 05 00 flags 0, 5 streams
Stream headers
Each stream header is an Offset (4 bytes, from the metadata root), a Size (4) and a Name, ending in a zero byte and padded with zeros to a multiple of 4 bytes. The streams:
| Name | Holds |
|---|---|
#~ | The metadata tables, compressed. |
#- | The same, uncompressed, as written for edit and continue. |
#Strings | Identifiers in UTF-8; see Heaps. |
#US | User strings, the string literals of the code. |
#GUID | GUIDs. |
#Blob | Signatures and other binary values. |
#Pdb | In Portable PDB files: the PDB ID and the entry point. |
UnityEngine.dll’s first headers are 6C 00 00 00 7C 0E 0B 00 23 7E 00 00
(#~ at 0x6C, 724,604 bytes) and E8 0E 0B 00 60 47 04
00 followed by #Strings and four zero bytes; then come
#US, #GUID and #Blob.
The tables stream
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | Reserved | 0. |
| 4 | 1 | u8 | MajorVersion | 2. |
| 5 | 1 | u8 | MinorVersion | 0. |
| 6 | 1 | u8 | HeapSizes | Bit 0: indexes into #Strings are 4 bytes, not 2. Bit 1: the same for #GUID. Bit 2: for #Blob. |
| 7 | 1 | u8 | Reserved | 1. |
| 8 | 8 | u64 | Valid | A bit for each table that is present. |
| 16 | 8 | u64 | Sorted | A bit for each table that is sorted. |
| 24 | 4 each | u32 | Rows | The number of rows of each present table, in table order. |
| – | varies | – | Tables | The rows of each table, one table after another. |
The tables, by number (ECMA-335 partition II, chapter 22):
| Numbers | Tables |
|---|---|
0x00–0x0F | Module, TypeRef, TypeDef, FieldPtr, Field, MethodPtr, MethodDef, ParamPtr, Param, InterfaceImpl, MemberRef, Constant, CustomAttribute, FieldMarshal, DeclSecurity, ClassLayout |
0x10–0x1F | FieldLayout, StandAloneSig, EventMap, EventPtr, Event, PropertyMap, PropertyPtr, Property, MethodSemantics, MethodImpl, ModuleRef, TypeSpec, ImplMap, FieldRVA, EncLog, EncMap |
0x20–0x2C | Assembly, AssemblyProcessor, AssemblyOS, AssemblyRef, AssemblyRefProcessor, AssemblyRefOS, File, ExportedType, ManifestResource, NestedClass, GenericParam, MethodSpec, GenericParamConstraint |
0x30–0x37 | The Portable PDB tables: Document, MethodDebugInformation, LocalScope, LocalVariable, LocalConstant, ImportScope, StateMachineMethod, CustomDebugInformation |
The size of each row depends on the heap sizes and on how many rows the tables it refers to have. The explorer shows the header and the row counts, lists the tables present in the Program tab, and keeps the rows as bytes: decoding them is not supported.
Heaps
#Strings– UTF-8 strings, each ending in a zero byte. Tables refer to them by offset.#US– user strings: each a compressed length, the string in UTF-16, and a final byte that is 1 when the string holds characters that ECMA-335 says need special handling, and 0 otherwise.#GUID– 16-byte GUIDs, referred to by index from 1.#Blob– each a compressed length and that many bytes.
A compressed length is 1 byte when it is below 0x80
(0xxxxxxx), 2 bytes when it is below 0x4000
(10xxxxxx …), and otherwise 4 bytes (110xxxxx …), high byte
first.
Managed resources
The Resources directory covers the resources embedded in the assembly, those listed in the ManifestResource table. Each is a 4-byte length and the data. In the files examined each started on an 8-byte boundary from the start of the block, an observation rather than a rule; the explorer reads up to 7 zero bytes of padding after each.
Strong name signature
A strongly named assembly has a signature here: a hash of the file, signed with the publisher’s private key, which the runtime can check against the public key in the Assembly table. Its size depends on the key, 128 bytes for a 1024-bit RSA key. It has nothing to do with an Authenticode certificate, which a file may have as well.
VTable fixups
Used by C++/CLI and other mixed-mode code to call managed methods from native code.
Each fixup is 8 bytes: an RVA (4) of a table of slots, a Count (2) of slots, and a Type
(2): 0x01 32-bit slots, 0x02 64-bit slots,
0x04 called from unmanaged code, 0x10 call the most derived
method. The runtime fills each slot with a callable address for the method token it
holds.