Back to course

The COFF Heritage

15 minbeginnerModule 1: FundamentalsContinues from pe-file-layout
15 min · Module 1: Fundamentals · Foundations

Why This Matters

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?

A Simple Model

Picture a factory pipeline:

  1. Object files (.obj). The compiler emits a COFF header, section headers, raw code and data, plus COFF relocations and a symbol table — everything the linker still needs to resolve. No DOS stub, no PE signature; SizeOfOptionalHeader should be zero.
  2. The linker merges the objects, applies relocations, picks final addresses, and writes an image: DOS stub, PE signature, the same 20-byte COFF header, now with an optional header attached, then the section table and section data.
  3. The image (.exe/.dll). The COFF header survives the trip unchanged in layout — only the surrounding context and a few field conventions differ (symbols zeroed out, optional header required).

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 Actual Detail

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

OffsetSizeFieldWho cares
02MachineLoader: reject wrong CPU; tools: pick a disassembler
22NumberOfSectionsEveryone: sizes the section table (loader caps it at 96)
44TimeDateStampTools, packaging: seconds since 1970-01-01 (C time_t)
84PointerToSymbolTableObject files: file offset of COFF symbols; should be 0 in images
124NumberOfSymbolsObject files: symbol count; should be 0 in images
162SizeOfOptionalHeaderEveryone: how many bytes follow before the section table
182CharacteristicsLoader: executable? DLL? 32-bit? large-address-aware?

Machine values you will actually meet (from the specification's Machine Types table):

ValueConstantTarget
0x14CIMAGE_FILE_MACHINE_I386Intel 386 and compatible (32-bit)
0x8664IMAGE_FILE_MACHINE_AMD64x64 (64-bit)
0xAA64IMAGE_FILE_MACHINE_ARM64ARM64 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.

Common mistake. Thinking the COFF header only exists in object files — or that it is a "pre-PE" leftover you can skip. It is not a leftover: it is the live identity block of every image, and it is the exact structure 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.

A Real Example

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

Fieldcli-64.execli-32.exe
Machine0x8664 — x640x014C — i386
NumberOfSections65
TimeDateStamp0x646827E40x646827DA
PointerToSymbolTable00
NumberOfSymbols00
SizeOfOptionalHeader0x00F0 (240 — PE32+)0x00E0 (224 — PE32)
Characteristics0x00220x0102

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.

Try It

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 2023

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

Check Your Understanding

Without looking back: which two COFF header fields should be zero in a modern image, and why?

Apply It

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.

Connect

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.