The CLR runtime header
Data directory 14 of the PE optional header gives the RVA of a 72-byte IMAGE_COR20_HEADER. A PE file without it is not a .NET file. The existing
CLR header page of the PE family describes it from the executable’s side; the fields are:
| Offset | Field | Size | Meaning |
|---|---|---|---|
| 0 | cb | 4 | The size of the header, 72. |
| 4 | MajorRuntimeVersion, MinorRuntimeVersion | 2 + 2 | 2.5 in every file written for the 2.0 runtime or later; 2.0 in the real .NET 1.0 and 1.1 files read here. |
| 8 | MetaData | 4 + 4 | RVA and size of the metadata root. |
| 16 | Flags | 4 | ILONLY 0x1, 32BITREQUIRED 0x2, IL_LIBRARY 0x4, STRONGNAMESIGNED 0x8, NATIVE_ENTRYPOINT 0x10, TRACKDEBUGDATA 0x10000, 32BITPREFERRED 0x20000 (see flags). |
| 20 | EntryPointToken | 4 | A MethodDef token (0x06……) or File token (0x26……); with NATIVE_ENTRYPOINT an RVA of native code. 0 for a library. |
| 24 | Resources | 4 + 4 | RVA and size of the managed resources: each is a 4-byte length and its bytes; the ManifestResource table names them and gives their offsets in this block. |
| 32 | StrongNameSignature | 4 + 4 | RVA and size of the signature (128 bytes for a 1024-bit key; see manifest). |
| 40 | CodeManagerTable | 4 + 4 | Reserved, zero. |
| 48 | VTableFixups | 4 + 4 | Records of 8 bytes (RVA, slot count, type word) used by C++/CLI exports: 0x1 32-bit slots, 0x2 64-bit slots, 0x4 from unmanaged code, 0x10 call most derived. |
| 56 | ExportAddressTableJumps | 4 + 4 | Reserved, zero. |
| 64 | ManagedNativeHeader | 4 + 4 | Precompiled native code: a ReadyToRun image has a header here that starts with the signature RTR. |
The metadata root
The metadata starts with a root, all little-endian:
| Offset | Field | Size | Meaning |
|---|---|---|---|
| 0 | Signature | 4 | 0x424A5342, the bytes BSJB. |
| 4 | MajorVersion, MinorVersion | 2 + 2 | 1 and 1. |
| 8 | Reserved | 4 | Zero. |
| 12 | Length | 4 | The length of the version string including its zero padding to a multiple of 4. |
| 16 | Version | Length | The version string: v4.0.30319, v2.0.50727, v1.1.4322, v1.0.3705 in the real files read here (see the overview). |
| … | Flags | 2 | Zero. |
| … | Streams | 2 | The number of stream headers. |
The explorer reports a root version other than 1.1, a version length that is not a multiple of 4 or runs past the metadata, and a missing signature as problems.
Stream headers and streams
Each stream header is an offset (from the start of the root), a size, and a zero-terminated name padded to a multiple of 4 bytes. The names:
| Stream | Holds |
|---|---|
#~ | The metadata tables, in the compressed form every compiler writes. |
#- | The same tables in the uncompressed form of edit-and-continue images, which may use the Ptr tables to put rows out of order. If both appear, the explorer reads #~. |
#Strings | Identifier strings, UTF-8, each ended by a zero byte; offset 0 is the empty string. |
#US | User strings: the string literals of the code (ldstr), each a compressed length, UTF-16 characters and a final byte. |
#GUID | 16-byte GUIDs, numbered from 1. |
#Blob | Binary data with a compressed length: signatures, constants, custom attribute values, marshalling descriptors, permission sets. |
#Pdb | Only in a portable PDB: see below. |
Stream sizes are multiples of 4 (the explorer notes a stream whose size is not, except #Pdb), a name may appear once, and streams should not overlap or start inside the root. Obfuscated files break these
rules; the explorer lists each broken rule as a problem on the stream and keeps reading, dropping an overlapping stream from the tree. The heaps are covered on the heaps page.
The header of the tables stream
| Offset | Field | Size | Meaning |
|---|---|---|---|
| 0 | Reserved | 4 | Zero. |
| 4 | MajorVersion, MinorVersion | 1 + 1 | The schema version: 2.0 in every file since the 2.0 runtime, 1.0 in the .NET 1.0 and 1.1 files read here. |
| 6 | HeapSizes | 1 | Bit 0: #Strings indexes are 4 bytes instead of 2. Bit 1: #GUID. Bit 2: #Blob. Bit 0x40: an extra 4-byte word follows the row counts (set by edit-and-continue writers). |
| 7 | Reserved | 1 | Compilers write 1. |
| 8 | Valid | 8 | A 64-bit vector: bit n is set when table n is present. |
| 16 | Sorted | 8 | A 64-bit vector: bit n is set when table n is sorted by its key column (the tables searched by binary search must be). |
| 24 | Rows | 4 each | One row count for each bit set in Valid, in table order, then the extra data word if HeapSizes has 0x40. |
The tables follow, each present table in order, its rows packed with no padding. The size of a row depends on HeapSizes and on the row counts of the tables its columns refer to: see tables and coded indexes. Up to a few bytes of zero padding end the stream. A set bit for a table number that has no known layout (0x2D to 0x2F, above 0x37) makes the rest of the stream unreadable: the explorer says so and shows the bytes.
The #Pdb stream
A portable PDB has the same root with the version string PDB v1.0 and an extra stream, #Pdb: a 20-byte PDB ID (a GUID and a 4-byte stamp that match the debug directory entry of the assembly), a 4-byte
entry point token, a 64-bit mask of the type system tables that are referenced, and one 4-byte row count for each set bit. The row counts size the columns of the debug tables that point into the assembly’s own tables (MethodDef, for instance).
Not described
Known, not yet covered
- Viewer: the edit-and-continue delta streams (the
#JTDstream and the extra-data word’s meaning) are shown as bytes; the ENCLog and ENCMap tables are shown as rows but not interpreted. Reference: the EnC rules are in the runtime’s documentation and are not described here.
Links
Reference
- ECMA-335 – partition II sections 24 and 25: the physical layout of metadata
- Portable PDB metadata – the #Pdb stream