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 32-bit PE file, even though it
runs as 64-bit code. An assembly is a PE32+ file when it is compiled for one 64-bit
processor, x64 (machine 0x8664) or ARM64 (0xAA64), or when
it holds precompiled native code for one, as ReadyToRun images and C++/CLI
assemblies do. The CLR header and the metadata are the same as in a 32-bit file; only
the PE headers around them change.
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. |
Flags
| 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. |
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. |
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.