Back to course

The Optional Header

15 minintermediateModule 2: HeadersContinues from pe-signature-coff
15 min · Module 2: Headers · Foundations

Why This Matters

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.

A Simple Model

Think of the optional header as the loader's instruction sheet, in three parts:

  1. Standard fields (first 24 bytes in PE32+, 28 in PE32) — the part the specification says is "defined for all implementations of COFF, including UNIX": magic, linker version, the three size fields, entry point, base of code (and base of data in PE32).
  2. Windows-specific fields — image base, both alignments, version numbers, image/header sizes, checksum, subsystem, DllCharacteristics, stack and heap sizes, loader flags, and NumberOfRvaAndSizes.
  3. Data directories — 16 address/size pairs (when 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.

The Actual Detail

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):

FieldSpec offset (PE32/PE32+)Sample A (PE32+)Sample B (PE32)
Magic00x20B — PE32+0x10B — PE32
Major/MinorLinkerVersion2 / 314.3614.36
SizeOfCode40x18000x1600
SizeOfInitializedData80x22000x1600
SizeOfUninitializedData1200
AddressOfEntryPoint160x1D400x1B87
BaseOfCode200x10000x1000
BaseOfData24 (PE32 only)absent — field does not exist0x3000
ImageBase28 / 24 (4 or 8 bytes)0x140000000 (8 bytes)0x00400000 (4 bytes)
SectionAlignment32 / 320x10000x1000
FileAlignment36 / 360x2000x200
MajorSubsystemVersion48 / 4866
SizeOfImage56 / 560x90000x7000
SizeOfHeaders60 / 600x4000x400
Subsystem68 / 683 — WINDOWS_CUI (console)3 — WINDOWS_CUI (console)
DllCharacteristics70 / 700x81600x8140
NumberOfRvaAndSizes92 / 10816 (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:

BitFlagIn A (0x8160)In B (0x8140)
0x0020HIGH_ENTROPY_VA — high-entropy 64-bit VA spaceyesno
0x0040DYNAMIC_BASE — relocatable at load time (ASLR)yesyes
0x0100NX_COMPAT — DEP/NX compatibleyesyes
0x8000TERMINAL_SERVER_AWAREyesyes

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.

Common mistake. Taking "optional" literally for executables. In an image this header is mandatory — the name survives from object files, where COFF really does omit it (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.

A Real Example

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:

  1. 0x118: 0b 02 → 0x20B → PE32+. From here on, parse with the 64-bit layout: ImageBase is 8 bytes, no BaseOfData.
  2. 0x11A: 0e 24 → linker 14.36. 0x11C: 00 18 00 00 → SizeOfCode 0x1800 — matches .text's SizeOfRawData.
  3. 0x128: 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.
  4. 0x130: 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.)
  5. 0x138: SectionAlignment 0x1000; 0x13C: FileAlignment 0x200 — the two rulers of PE geometry.
  6. 0x15C: 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.

Try It

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	00008160

Now 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.

Check Your Understanding

Without looking back: why is this header called "optional," and when is it actually optional?

Apply It

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.

Connect

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.