A PE32+ file starts as an MS-DOS program, exactly as a 32-bit one does. Its first 64 bytes are an MS-DOS header, and
a small DOS program, the stub, follows it. Windows reads only two fields of the header:
the MZ signature and e_lfanew, which says where the PE
headers start. Between the stub and the PE headers, Microsoft’s linker also writes the
Rich header, an undocumented list of the tools that built the program. All numbers are
little-endian.
The MS-DOS header
IMAGE_DOS_HEADER, 64 bytes. Apart from e_magic and
e_lfanew, its fields describe the DOS stub and are the same in almost
every file. They have the same meaning as in an
MS-DOS program.
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
0x00 | 2 | char[2] | e_magic | MZ (4D 5A). |
0x02 | 2 | u16 | e_cblp | Bytes used in the last 512-byte page of the DOS program. |
0x04 | 2 | u16 | e_cp | 512-byte pages in the DOS program. |
0x06 | 2 | u16 | e_crlc | Number of DOS relocation entries, usually 0. |
0x08 | 2 | u16 | e_cparhdr | Header size in 16-byte paragraphs: 4, for 64 bytes. |
0x0A | 2 | u16 | e_minalloc | Minimum extra paragraphs the DOS program needs. |
0x0C | 2 | u16 | e_maxalloc | Maximum extra paragraphs; 0xFFFF asks for all there is. |
0x0E | 2 | u16 | e_ss | Initial SS, relative to the DOS program. |
0x10 | 2 | u16 | e_sp | Initial SP. |
0x12 | 2 | u16 | e_csum | Checksum, usually 0. |
0x14 | 2 | u16 | e_ip | Initial IP. |
0x16 | 2 | u16 | e_cs | Initial CS, relative to the DOS program. |
0x18 | 2 | u16 | e_lfarlc | File offset of the DOS relocation table, usually 0x40. |
0x1A | 2 | u16 | e_ovno | Overlay number, 0. |
0x1C | 8 | u16[4] | e_res | Reserved, zero. |
0x24 | 2 | u16 | e_oemid | OEM identifier. |
0x26 | 2 | u16 | e_oeminfo | OEM information. |
0x28 | 20 | u16[10] | e_res2 | Reserved, zero. |
0x3C | 4 | u32 | e_lfanew | File offset of the PE signature; see below. |
00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00
00000010 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00000030 00 00 00 00 00 00 00 00 00 00 00 00 F0 00 00 00
MZ, e_cblp 0x90, e_cp 3, e_cparhdr 4, e_maxalloc 0xFFFF, e_sp 0xB8, e_lfarlc 0x40, and e_lfanew 0xF0e_lfanew
The 32-bit value at 0x3C is the file offset of the PE signature,
PE\0\0, and so of everything after it. MS-DOS ignores it. Its value
depends on the size of the stub and of the Rich header: in the 64-bit glfw3.dll it is
0xF0.
The explorer turns the file down when e_lfanew is below
0x40, because the PE header would then overlap the DOS header. Windows
itself accepts such files, and some hand-made tiny programs rely on it; the explorer
does not lay them out. It also turns the file down when e_lfanew leaves
less than 24 bytes before the end of the file.
The DOS stub
The stub is a complete MS-DOS program, so that running a Windows program under DOS
prints a message instead of crashing. The header’s fields describe it: with
e_cparhdr 4, the program starts at 0x40. Microsoft’s linker writes the
same 14 bytes of 16-bit code and a message, unless a different stub is given with
/STUB:
00000040 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68
00000050 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F
00000060 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20
00000070 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00
0D 0D 0A 24, then seven zero bytes| Bytes | Instruction | What it does |
|---|---|---|
0E | push cs | Copies CS to DS, so that DS points at the stub. |
1F | pop ds | |
BA 0E 00 | mov dx, 0x000E | The message is 14 bytes into the stub. |
B4 09 | mov ah, 0x09 | DOS function 9: print the text at DS:DX up to the $. |
CD 21 | int 0x21 | |
B8 01 4C | mov ax, 0x4C01 | DOS function 0x4C: exit, with code 1. |
CD 21 | int 0x21 |
The explorer recognises these 14 bytes and splits the stub into its code, the message
and the padding after it; it shows any other stub as bytes. The 16-bit instructions
are described in the x86 instruction reference.
Wine’s own DLLs put the text Wine builtin DLL at 0x40
instead of a program.
The Rich header
Microsoft’s linker, from Visual C++ 5 on, writes a block between the stub and the PE
signature that lists the tools whose output went into the program: each compiler,
assembler, linker and library tool, with its build number and how many objects it
made. Microsoft has never documented it. It is called the Rich header after the word
Rich that ends it. GNU ld (MinGW, Cygwin, Wine), LLVM’s lld, and the
Borland and Watcom linkers write none.
Every dword is XORed with a 32-bit key, except the word Rich and the key
itself, which follow the entries:
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | DanS ^ key | The start marker: DanS (0x536E6144) XORed with the key. |
| 4 | 12 | u32[3] | Padding | Three zero dwords XORed with the key, so each equals the key. |
| 16 | 8 × n | u32 pairs | Entries | One entry for each tool; see Entries. |
| 16 + 8n | 4 | char[4] | Rich | The end marker, not masked. |
| 20 + 8n | 4 | u32 | Key | The XOR key, which is also a checksum; see Checksum. |
Zero bytes follow, up to e_lfanew. To find the header, search backwards
from e_lfanew for Rich, read the key after it, then search
backwards for a dword equal to 0x536E6144 XOR the key.
Entries
Each entry is two dwords, both XORed with the key:
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | u32 | comp.id | The product ID in the high 16 bits and the tool’s build number in the low 16. |
| 4 | 4 | u32 | count | How many objects in the link that tool made. |
00000080 81 BB C1 9A C5 DA AF C9 C5 DA AF C9 C5 DA AF C9
00000090 CC A2 3C C9 C9 DA AF C9 20 83 AE C8 C7 DA AF C9
000000A0 20 83 AC C8 C6 DA AF C9 20 83 AA C8 CC DA AF C9
000000B0 20 83 AB C8 CF DA AF C9 18 25 64 C9 CC DA AF C9
000000C0 C5 DA AE C9 BB DA AF C9 37 83 AB C8 C9 DA AF C9
000000D0 37 83 AF C8 C4 DA AF C9 37 83 AD C8 C4 DA AF C9
000000E0 52 69 63 68 C5 DA AF C9 00 00 00 00 00 00 00 00
0x80, with the key 0xC9AFDAC5. 0x9AC1BB81 XOR the key is 0x536E6144, DanS. The first entry, 0xC93CA2CC and 0xC9AFDAC9, unmasks to comp.id 0x00937809 and count 12.The ten entries of that file, unmasked:
| Product ID | Name | Build | Objects |
|---|---|---|---|
0x0093 | Implib900 | 30729 | 12 |
0x0101 | Implib1400 | 23013 | 2 |
0x0103 | Masm1400 | 23013 | 3 |
0x0105 | Utc1900_CPP | 23013 | 9 |
0x0104 | Utc1900_C | 23013 | 10 |
0x00CB | Implib1100 | 65501 | 9 |
0x0001 | Import0 | 0 | 126 |
0x0104 | Utc1900_C | 23026 | 12 |
0x0100 | Export1400 | 23026 | 1 |
0x0102 | Linker1400 | 23026 | 1 |
Product IDs
The product ID names the tool and its version. The explorer names IDs
0x0000 to 0x010E from the table of the richheader project.
The names are Microsoft’s internal ones:
- Utc – the C and C++ compiler; Utc1900 is Visual C++ 14.x, from Visual Studio 2015 on.
- Masm – the assembler. Linker – the linker. Cvtres – the resource converter. Export and Implib – the tools that make export files and import libraries.
- AliasObj, Cvtpgd, Cvtomf, Phx (Phoenix), ILAsm, VisualBasic60 and Resource – other tools.
- Import0 – counts imported functions in older linkers.
A suffix says what the compiler made: _C and _CPP for C and
C++, _LTCG for link-time code generation, _POGO_I and
_POGO_O for profile-guided builds (instrumented and optimised), and
_CVTCIL for C++/CLI.
The ranges by Visual Studio release, as the richheader project gives them; it calls them “somewhat guessed”:
| Product IDs | Visual Studio |
|---|---|
0x00FD–0x010E | 2015 and later (14.x) |
0x00EB–0x00FC | 2013 Update (12.10) |
0x00D9–0x00EA | 2013 (12.00) |
0x00C7–0x00D8 | 2012 (11.00) |
0x00B5–0x00C6 | 2010 SP1 (10.10) |
0x0098–0x00B4 | 2010 (10.00) |
0x0083–0x0097 | 2008 (9.00) |
0x006D–0x0082 | 2005 (8.00) |
0x005A–0x006C | .NET 2003 (7.10) |
Every Visual Studio from 2015 on uses the IDs 0x00FD to
0x010E; only the build number tells them apart. The richprint project
lists builds: 0x01048DA4, for example, is the C compiler, build 36260,
from Visual Studio 2026 18.10.2. The source table lists 0x00D3 twice; the
second is plainly 0x00E3 (Utc1800_CVTCIL_CPP), and the explorer uses it
there.
Checksum
The key is a checksum of the start of the file and of the entries. Everything is
added modulo 232, and rol32 rotates a 32-bit value left:
sum = offset of DanS
for each byte i before DanS, except the four bytes of e_lfanew (0x3C to 0x3F):
sum = sum + rol32(byte[i], i mod 32)
for each entry:
sum = sum + rol32(comp.id, count mod 32)
key = sum
Every file examined for this site from Visual C++ 6 and later linkers matches this
sum, the 64-bit glfw3.dll among them. Every file from linkers 5.10 and 5.12 (Visual C++ 5) has a
key exactly 0x884F33A1 less than the sum. Why is not known; it may be
that the early linker summed something slightly different, but that is an inference.
The explorer accepts both and says which it found.