You learned that a PE file begins with MZ and contains a PE signature somewhere later. That is the entry point into the format — not the format itself. Before decoding any single field, you need the whole map: which layers exist, in what order they sit on disk, and where one ends and the next begins.
Everyone who touches an executable needs this map. The Windows loader walks it in order — DOS header, PE header, section table — and then maps each section's raw bytes into memory. A debugger or disassembler walks it to find code. A packer, a code-signing tool, or a malware analyst rewrites parts of it. If you misplace even one layer — say, you trust a wrong SizeOfOptionalHeader — every section row after it is garbage and the file will not load.
Think of the file as three zones front to back:
0x3C..text, .rdata, .data, and the rest, each located by its own PointerToRawData and padded to FileAlignment.The model's missing piece: nothing in the instruction block stores a single master pointer to "the data." Section data is found only through the section table — one row at a time, each row carrying its own file offset. No table, no payload.
The PE specification fixes this order: MS-DOS stub, PE signature, COFF file header, optional header, section headers, then image pages. Here is the exact byte map of a real 64-bit PE (setuptools' cli-64.exe, 14336 bytes, 6 sections):
| Layer | File range | Size | What it is |
|---|---|---|---|
| MS-DOS header | 0x0000–0x003F | 64 | e_magic = MZ; e_lfanew at 0x3C = 0x100 |
| MS-DOS stub | 0x0040–0x00FF | 96 | 16-bit stub that prints "This program cannot be run in DOS mode." |
| PE signature | 0x0100–0x0103 | 4 | 50 45 00 00 — "PE" plus two nulls |
| COFF header | 0x0104–0x0117 | 20 | Machine, section count, timestamp, sizes, characteristics |
| Optional header | 0x0118–0x0207 | 240 (0xF0) | Entry point, image base, alignments, subsystem, 16 data directories |
| Section table | 0x0208–0x02F7 | 240 (6 × 40) | One 40-byte row per section |
| Header padding | 0x02F8–0x03FF | 104 | Zeros out to SizeOfHeaders = 0x400 |
| .text raw | 0x0400–0x1BFF | 0x1800 | Code; entry point resolves to file 0x1140 |
| .rdata raw | 0x1C00–0x2FFF | 0x1400 | Read-only data (imports live here) |
| .data raw | 0x3000–0x31FF | 0x200 | Initialized data |
| .pdata raw | 0x3200–0x33FF | 0x200 | x64 exception table |
| .rsrc raw | 0x3400–0x35FF | 0x200 | Resources |
| .reloc raw | 0x3600–0x37FF | 0x200 | Base relocations; file ends at 0x3800 = 14336 |
Notice the two alignments at work: FileAlignment = 0x200 pads every raw section start on disk (0x400, 0x1C00, 0x3000…), while SectionAlignment = 0x1000 spaces sections in memory (0x1000, 0x3000, 0x5000…). Headers are zero-padded so the first section begins exactly at the 0x400 SizeOfHeaders boundary.
Compared with ELF: both formats put headers first and payload last, but the mechanics differ. ELF always has a fixed-size ELF header at byte 0 and finds its tables through explicit pointers (e_phoff, e_shoff) — the section header table often sits at the end of the file. PE has no operative header at byte 0 (that is the DOS layer), its section table is found by arithmetic, not by a pointer field, and it lives inside the header region — immediately after the optional header — never at the end. ELF also has a separate program-header table describing loadable segments; PE has none: the section table plus the optional header's alignment and size fields are the loader's entire map.
e_lfanew (0x100) + 4 (signature) + 20 (COFF) + SizeOfOptionalHeader (0xF0). Change any of those and the section table moves. Compute it; never hardcode it.The whole header region of cli-64.exe, at the layer boundaries:
00000000: 4d5a 9000 0300 0000 0400 0000 ffff 0000 MZ.............. 00000010: b800 0000 0000 0000 4000 0000 0000 0000 ........@....... 00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000030: 0000 0000 0000 0000 0000 0000 0001 0000 ................ 00000100: 5045 0000 6486 0600 e427 6864 0000 0000 PE..d....'hd.... 00000110: 0000 0000 f000 2200 ......".
Walk-through of the seams:
00 01 00 00 — little-endian 0x100 — so the instruction block starts at offset 0x100.50 45 00 00.f000 2200: SizeOfOptionalHeader = 0xF0, so the optional header spans 0x118 to 0x207, and the section table starts at 0x208 — six rows of 40 bytes end at 0x2F8.PointerToRawData is 0x400. Bytes 0x2F8–0x3FF are the zero pad, and the payload begins at 0x400. The last section (.reloc) ends at 0x3800, which is exactly the file size — no trailing data.00000208: 2e74 6578 7400 0000 bc17 0000 0010 0000 .text........... 00000218: 0018 0000 0004 0000 0000 0000 0000 0000 ................ 00000228: 0000 0000 2000 0060 .... ..`.
That is the first section row: name .text, VirtualSize 0x17BC, VA 0x1000, SizeOfRawData 0x1800, PointerToRawData 0x400, Characteristics 0x60000020. The map and the table agree — that is the whole contract.
Any Windows executable works. On Linux or macOS, dump the layer boundaries, then let objdump reconstruct the section table for you:
$ xxd -s 0x100 -l 24 cli-64.exe
00000100: 5045 0000 6486 0600 e427 6864 0000 0000 PE..d....'hd....
00000110: 0000 0000 f000 2200 ......".
$ objdump -h cli-64.exe
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000017bc 0000000140001000 0000000140001000 00000400 2**4
1 .rdata 0000132c 0000000140003000 0000000140003000 00001c00 2**4
2 .data 00000200 0000000140005000 0000000140005000 00003000 2**4
...What to look for: the File off column is PointerToRawData straight from the section table, and every value is a multiple of 0x200 — FileAlignment made visible. The VMA column shows the same sections at 0x1000-spaced addresses — SectionAlignment.
Without looking back: in what order do a PE file's layers appear on disk?
A PE file has e_lfanew = 0x100, SizeOfOptionalHeader = 0xF0, and 6 sections. Where does the section table start, and where does it end?
The pattern: every PE offset you compute chains from e_lfanew forward through sizes. None of them are constants.
You now have the map. The instruction block in the middle of that map has two identities: the 20-byte COFF header that PE inherited from the Common Object File Format — the same header that opens every .obj file — and the optional header that only images carry. The headers themselves come next, layer by layer.
Next: The COFF Heritage — why a Windows executable carries a Unix-lineage object-file header, and what the linker puts in it.