The COFF header says what the file is. The optional header says how to load it: where execution begins (AddressOfEntryPoint), where the image prefers to live in memory (ImageBase), how sections align on disk versus in RAM (FileAlignment / SectionAlignment), which window subsystem it needs (console or GUI), and — at its tail — the 16 data directories that locate imports, exports, relocations, and resources. Lose this header and the loader has no instructions; it cannot place a single byte.
It is also where PE32 and PE32+ genuinely diverge: field widths change (ImageBase grows from 4 bytes to 8), one field disappears (BaseOfData), and the total size shifts from 224 to 240 bytes. Reading these two layouts side by side is how you learn to parse any PE regardless of bitness.
Think of the optional header as the loader's instruction sheet, in three parts:
NumberOfRvaAndSizes.NumberOfRvaAndSizes = 16) pointing at the import table, export table, relocations, and so on. That is the next concept.Why "optional"? The specification answers directly: the header "is optional in the sense that some files (specifically, object files) do not have it. For image files, this header is required." COFF object files skip it — SizeOfOptionalHeader is zero there. Executables and DLLs cannot.
The model's missing piece: Magic decides which of two layouts you are parsing. 0x10B means PE32, 0x20B means PE32+ — everything after byte 1 depends on it.
Every major field, from Magic through NumberOfRvaAndSizes, with both samples' verified values (A = PE32+ cli-64.exe at file 0x118; B = PE32 cli-32.exe at file 0x118):
| Field | Spec offset (PE32/PE32+) | Sample A (PE32+) | Sample B (PE32) |
|---|---|---|---|
| Magic | 0 | 0x20B — PE32+ | 0x10B — PE32 |
| Major/MinorLinkerVersion | 2 / 3 | 14.36 | 14.36 |
| SizeOfCode | 4 | 0x1800 | 0x1600 |
| SizeOfInitializedData | 8 | 0x2200 | 0x1600 |
| SizeOfUninitializedData | 12 | 0 | 0 |
| AddressOfEntryPoint | 16 | 0x1D40 | 0x1B87 |
| BaseOfCode | 20 | 0x1000 | 0x1000 |
| BaseOfData | 24 (PE32 only) | absent — field does not exist | 0x3000 |
| ImageBase | 28 / 24 (4 or 8 bytes) | 0x140000000 (8 bytes) | 0x00400000 (4 bytes) |
| SectionAlignment | 32 / 32 | 0x1000 | 0x1000 |
| FileAlignment | 36 / 36 | 0x200 | 0x200 |
| MajorSubsystemVersion | 48 / 48 | 6 | 6 |
| SizeOfImage | 56 / 56 | 0x9000 | 0x7000 |
| SizeOfHeaders | 60 / 60 | 0x400 | 0x400 |
| Subsystem | 68 / 68 | 3 — WINDOWS_CUI (console) | 3 — WINDOWS_CUI (console) |
| DllCharacteristics | 70 / 70 | 0x8160 | 0x8140 |
| NumberOfRvaAndSizes | 92 / 108 | 16 (file offset 0x184) | 16 (file offset 0x174) |
| Total header size | — | 0xF0 (240) | 0xE0 (224) |
Subsystem, from the spec: 2 = IMAGE_SUBSYSTEM_WINDOWS_GUI (graphical), 3 = IMAGE_SUBSYSTEM_WINDOWS_CUI (character/console). Both our samples are 3 — they are console tools, which is why file reports "(console)".
DllCharacteristics 0x8160 (Sample A), decomposed against the spec's DLL Characteristics flags:
| Bit | Flag | In A (0x8160) | In B (0x8140) |
|---|---|---|---|
| 0x0020 | HIGH_ENTROPY_VA — high-entropy 64-bit VA space | yes | no |
| 0x0040 | DYNAMIC_BASE — relocatable at load time (ASLR) | yes | yes |
| 0x0100 | NX_COMPAT — DEP/NX compatible | yes | yes |
| 0x8000 | TERMINAL_SERVER_AWARE | yes | yes |
0x0020 | 0x0040 | 0x0100 | 0x8000 = 0x8160 exactly; Sample B drops only HIGH_ENTROPY_VA (0x8140), which makes sense — a PE32 image does not manage a 64-bit high-entropy address space.
SizeOfOptionalHeader = 0). Another trap: reading DllCharacteristics 0x8160 with the COFF Characteristics table from the previous concept. Different field, different flags: 0x0020 here is HIGH_ENTROPY_VA, but in the COFF header 0x0020 was LARGE_ADDRESS_AWARE. Always know which header you are standing in.Sample A's optional header, first 80 bytes from file offset 0x118:
00000118: 0b02 0e24 0018 0000 0022 0000 0000 0000 ...$....."...... 00000128: 401d 0000 0010 0000 0000 0040 0100 0000 @..........@.... 00000138: 0010 0000 0002 0000 0600 0000 0000 0000 ................ 00000148: 0600 0000 0000 0000 0090 0000 0004 0000 ................ 00000158: 0000 0000 0300 6081 0000 1000 0000 0000 ......`.........
Walk every major field:
0b 02 → 0x20B → PE32+. From here on, parse with the 64-bit layout: ImageBase is 8 bytes, no BaseOfData.0e 24 → linker 14.36. 0x11C: 00 18 00 00 → SizeOfCode 0x1800 — matches .text's SizeOfRawData.40 1d 00 00 → AddressOfEntryPoint 0x1D40 (RVA). .text has VA 0x1000 at file 0x400, so file offset = 0x400 + (0x1D40 − 0x1000) = 0x1140 — and the bytes there are 48 83 ec 28 (sub rsp, 0x28): the first instruction Windows executes.00 00 00 40 01 00 00 00 → 8-byte little-endian ImageBase 0x140000000 — the preferred load address. (Sample B's 4-byte field reads 00 00 40 00 → 0x00400000, the classic 32-bit default.)03 00 → Subsystem 3 → console. 0x15E: 60 81 → 0x8160 → the four DllCharacteristics flags decoded above.Bytes past the ones above hold stack/heap sizes (each 0x100000 reserve, 0x1000 commit), LoaderFlags = 0, then NumberOfRvaAndSizes = 16 at file offset 0x184 — the gate to the 16 data directories that start at 0x188.
Let objdump read the optional header for you — these are its real lines for Sample A:
$ objdump -x cli-64.exe | grep -E 'Magic|AddressOfEntry|Subsystem|DllChar|Alignment'
Magic 020b (PE32+)
AddressOfEntryPoint 0000000000001d40
SectionAlignment 00001000
FileAlignment 00000200
Subsystem 00000003 (Windows CUI)
DllCharacteristics 00008160Now hex-dump the raw bytes and find Magic yourself:
$ xxd -s 0x118 -l 48 cli-64.exe
00000118: 0b02 0e24 0018 0000 0022 0000 0000 0000 ...$....."......
00000128: 401d 0000 0010 0000 0000 0040 0100 0000 @..........@....
00000138: 0010 0000 0002 0000 0600 0000 0000 0000 ................What to look for: the first two bytes (0b 02 = 0x20B) pick the layout before you read anything else, and every field you just saw in objdump is sitting at its spec offset in that dump — Magic at 0, entry point at 16 (40 1d 00 00), SectionAlignment at 32.
Without looking back: why is this header called "optional," and when is it actually optional?
Sample A's DllCharacteristics is 0x8160. Which flags are set?
The pattern: Magic chooses the layout, entry point names the first instruction, alignments rule the geometry, Subsystem picks the window type, DllCharacteristics grades the hardening — and NumberOfRvaAndSizes unlocks the data directories.
The optional header ends by telling you how many data directories follow — 16 in both samples — and each directory is an address/size pair pointing at a table the loader or the OS needs: imports, exports, relocations, resources, and twelve more. That address book is the next layer of the format.
Next: The Data Directories — the 16 slots, what each one locates, and how the loader uses them before your first line of code runs.