Straight after the 4-byte PE signature sits a 20-byte structure the specification calls the COFF file header — Common Object File Format. This is not a Windows-only invention. The Microsoft PE/COFF specification describes both file kinds in one document, and when it introduces the optional header's standard fields it says outright they are "defined for all implementations of COFF, including UNIX." The object-file lineage is Microsoft's own wording, not folklore.
It matters because the same 20 bytes open a .obj the linker is about to consume and sit inside every finished .exe the loader will run. If you understand this header once, you understand it in both worlds. It is also where the loader makes its first serious decisions: which CPU, how many sections, is this thing even executable?
Picture a factory pipeline:
SizeOfOptionalHeader should be zero.The model's missing piece: which fields mean what depends on which side of the linker you are on. The bytes are identical; the contract is not.
The COFF file header is 20 bytes, and the specification gives its layout exactly (it begins at file offset 0 in an object file, or right after the PE signature in an image):
| Offset | Size | Field | Who cares |
|---|---|---|---|
| 0 | 2 | Machine | Loader: reject wrong CPU; tools: pick a disassembler |
| 2 | 2 | NumberOfSections | Everyone: sizes the section table (loader caps it at 96) |
| 4 | 4 | TimeDateStamp | Tools, packaging: seconds since 1970-01-01 (C time_t) |
| 8 | 4 | PointerToSymbolTable | Object files: file offset of COFF symbols; should be 0 in images |
| 12 | 4 | NumberOfSymbols | Object files: symbol count; should be 0 in images |
| 16 | 2 | SizeOfOptionalHeader | Everyone: how many bytes follow before the section table |
| 18 | 2 | Characteristics | Loader: executable? DLL? 32-bit? large-address-aware? |
Machine values you will actually meet (from the specification's Machine Types table):
| Value | Constant | Target |
|---|---|---|
| 0x14C | IMAGE_FILE_MACHINE_I386 | Intel 386 and compatible (32-bit) |
| 0x8664 | IMAGE_FILE_MACHINE_AMD64 | x64 (64-bit) |
| 0xAA64 | IMAGE_FILE_MACHINE_ARM64 | ARM64 little endian |
Object versus image conventions. The specification is explicit: SizeOfOptionalHeader "is required for executable files but not for object files" and should be zero for an object; PointerToSymbolTable and NumberOfSymbols "should be zero for an image because COFF debugging information is deprecated." NumberOfSections still drives everything — but the Windows loader refuses more than 96 sections.
TimeDateStamp. The low 32 bits of seconds since 00:00 January 1, 1970 — a C runtime time_t. The specification adds two caveats: a stamp of 0 or 0xFFFFFFFF "does not represent a real or meaningful date/time stamp," and for exceptions it points to IMAGE_DEBUG_TYPE_REPRO in the Debug Type section (the mechanism reproducible-build tooling uses instead of a wall-clock stamp). This file's stamp, 0x646827E4, decodes to 2023-05-20 01:52:36 UTC.
objdump, dumpbin, and the Windows loader itself read immediately after the PE signature. Skipping it means you never learn the CPU, the section count, or whether the file is executable at all.The same 20 bytes in two real files — cli-64.exe (PE32+) and cli-32.exe (PE32), both starting at file offset 0x104:
00000104: 6486 0600 e427 6864 0000 0000 0000 0000 d....'hd........ 00000114: f000 2200 ..". 00000104: 4c01 0500 da27 6864 0000 0000 0000 0000 L....'hd........ 00000114: e000 0201 ....
Field-by-field (all multi-byte values little-endian):
| Field | cli-64.exe | cli-32.exe |
|---|---|---|
| Machine | 0x8664 — x64 | 0x014C — i386 |
| NumberOfSections | 6 | 5 |
| TimeDateStamp | 0x646827E4 | 0x646827DA |
| PointerToSymbolTable | 0 | 0 |
| NumberOfSymbols | 0 | 0 |
| SizeOfOptionalHeader | 0x00F0 (240 — PE32+) | 0x00E0 (224 — PE32) |
| Characteristics | 0x0022 | 0x0102 |
Read the conventions in the data: both symbol fields are zero (COFF debugging deprecated for images), both timestamps land ten seconds apart on 2023-05-20, and SizeOfOptionalHeader already tells you 64-bit versus 32-bit before you ever open the optional header — 240 bytes versus 224. The two files were built by the same linker (both optional headers report 14.36) targeting different machines.
Any PE or COFF file works. Dump the 20 bytes yourself, then let objdump decode them:
$ xxd -s 0x104 -l 20 cli-64.exe
00000104: 6486 0600 e427 6864 0000 0000 0000 0000 d....'hd........
00000114: f000 2200 ..".
$ objdump -x cli-64.exe | head -8
Characteristics 0x22
executable
large address aware
Time/Date Sat May 20 06:52:36 2023What to look for: the first two bytes are the machine in little-endian (64 86 = 0x8664 = x64), bytes 16–19 are SizeOfOptionalHeader and Characteristics (f0 00 then 22 00), and objdump's "executable / large address aware" lines are exactly the bits of 0x0022 — decoded for you.
Without looking back: which two COFF header fields should be zero in a modern image, and why?
You dump a PE's COFF header and read the first two bytes as 4c 01. What machine is it, and what is NumberOfSections if the next two bytes are 05 00?
The pattern: read Machine first to pick the decoder, NumberOfSections to size the table, SizeOfOptionalHeader to find the section table — in that order, every time.
The COFF header is the modern half of the file's dual identity. The other half sits in front of it: a full 64-byte MS-DOS header that still opens every PE, carrying the magic MZ and the pointer that led you here.
Next: The MS-DOS Header — all 64 bytes, field by field, and why a 40-year-old header still gates every Windows executable.