Back to course

The File Header

16 minbeginnerModule 1: The Relocatable ObjectContinues from coff-intro
16 min · Module 1: The Relocatable Object · Structures

Why This Matters

Seven fields in twenty bytes, and every parser needs all seven before it can read anything else. NumberOfSections says how long the section table is. PointerToSymbolTable says where the symbols are. SizeOfOptionalHeader says whether this file is an object or an image.

It is a small structure, which means it is easy to get subtly wrong in ways that do not announce themselves. Two of the seven fields are 16-bit and two are 32-bit, the section table starts immediately after, and there is no magic number to catch you.

A Simple Model

Read it as three questions, in this order:

  1. What am I?Machine and SizeOfOptionalHeader. The first says which CPU; the second says object or image.
  2. What is in the file?NumberOfSections, PointerToSymbolTable, NumberOfSymbols.
  3. When and how was it made?TimeDateStamp and Characteristics.

The order matters for one specific reason: SizeOfOptionalHeader is how you know whether the section table starts at offset 20 or at offset 20 + SizeOfOptionalHeader. In an object it starts at 20. In a PE image it starts after the optional header. Get this wrong and every section header you read is garbage.

The Actual Detail

All seven fields, decoded from the bytes of sample_msvc.obj:

OffsetSizeFieldBytesValueMeaning
0x002Machine64 860x8664IMAGE_FILE_MACHINE_AMD64
0x022NumberOfSections09 009section table has 9 records
0x044TimeDateStamp2a a5 b6 6a0x6AB6A52Aseconds since the Unix epoch
0x084PointerToSymbolTable48 03 00 000x348file offset of the symbol table
0x0c4NumberOfSymbols1f 00 00 003118-byte records, aux included
0x102SizeOfOptionalHeader00 000no optional header: this is an object
0x122Characteristics00 000none set

Every one of those values was decoded independently from the raw bytes and then required to match llvm-readobj --file-headers, which reports SectionCount: 9, PointerToSymbolTable: 0x348, SymbolCount: 31 and OptionalHeaderSize: 0. Agreement was required before the table above was written down.

Three fields deserve more than a table row.

NumberOfSymbols counts aux records. It is a count of 18-byte slots, not a count of names. A symbol that owns one aux record occupies two slots. Our file has 31 slots and, once you skip past the aux records, far fewer actual names. A parser that treats 31 as "31 symbols" will read 18 bytes of garbage as a symbol and then lose sync. The rule is: read a record, read its NumberOfAuxSymbols, advance by 1 + NumberOfAuxSymbols slots.

TimeDateStamp is a Unix timestamp in an otherwise unremarkable little-endian file. Ours is 0x6AB6A52A. This is not a COFF invention — it is the same epoch as the ELF header, and the same field you saw in the ELF course's own hex dumps. Cross-format consistency is rarer than you would expect, and worth noticing when it happens.

Characteristics is zero here, and that is normal for an object. This field exists for images, where it carries IMAGE_FILE_EXECUTABLE_IMAGE and IMAGE_FILE_DLL. An object is neither, so it sets none of them. Do not read its emptiness as a missing field.

The machine types

Machine is the field that tells you how wide a pointer is, which changes how you interpret almost everything else. The three targets used in this course:

ValueNamePointer sizeSeen in this course
0x014CIMAGE_FILE_MACHINE_I38632-bitsample_m32.obj
0x8664IMAGE_FILE_MACHINE_AMD6464-bitsample_msvc.obj, sample_gnu.obj
0xAA64IMAGE_FILE_MACHINE_ARM6464-bitthe PE course's cli-64.exe family

Others you will meet in the PE course include 0x01C4 (ARMNT, 32-bit ARM), 0x0200 (IA64, Itanium) and 0xAA64 (ARM64). llvm-readobj prints the address size alongside the machine, which is the quickest sanity check available: a file claiming AMD64 with AddressSize: 32bit is lying to you about something.

A Real Example

Decode the header by hand, then check. Here are the first 20 bytes again, this time with the reading written under each field:

00000000: 6486 0900 2aa5 b66a 4803 0000 1f00 0000
                 |---| |--| |----| |--------| |--------|
                0x8664   9     ts     0x348        31

00000010: 0000 0000
                 |--| |--|
               opt=0  ch=0

Now the check that proves the field boundaries are right rather than merely plausible. NumberOfSections is 9, so the section table is 9 records of 40 bytes = 360 bytes, running from 0x14 to 0x17C. And 0x17C is exactly where the .text raw data begins, according to the first section header:

$ xxd -s 0x14 -l 4 sample_msvc.obj
00000014: 2e74 6578                                .tex
$ xxd -s 0x17c -l 8 sample_msvc.obj
0000017c: 50 00 00 00 00 00 00 00                  P.......
$ llvm-readobj --sections sample_msvc.obj | head -8
    Number: 1
    Name: .text (2E 74 65 78 74 00 00 00)
    RawDataSize: 108
    PointerToRawData: 0x17C

0x14 + 360 = 0x17C. Two independently derived numbers — the section count times the record size, and the raw data pointer read out of the first section header — land on the same offset. That is what a correct header looks like.

Do the same arithmetic for the symbol table: PointerToSymbolTable = 0x348 and NumberOfSymbols = 31, so 31 × 18 = 558 = 0x22E, putting the end at 0x348 + 0x22E = 0x576. And 0x576 is where the string table starts — which is where the file utility's symbol offset=0x348 was pointing all along.

Try It Yourself

$ clang --target=x86_64-pc-windows-msvc -c sample.c -o sample_msvc.obj
$ xxd -l 20 sample_msvc.obj
$ llvm-readobj --file-headers sample_msvc.obj
$ file sample_msvc.obj

Confirm the three cross-checks, because they are the whole habit of this concept:

Then see the machine field actually matter:

$ clang --target=i686-pc-windows-msvc -c sample.c -o sample_m32.obj
$ llvm-readobj --file-headers sample_m32.obj | grep -E 'Machine|AddressSize'

The same source now reports IMAGE_FILE_MACHINE_I386 (0x14C) and AddressSize: 32bit. Nothing about the container changed; the CPU did.

Check Your Understanding

Without looking back: NumberOfSymbols is 31 and PointerToSymbolTable is 0x348. Where does the symbol table end, and what begins there?

Apply It

You are writing a tool that must handle both COFF objects and PE images. You read NumberOfSections = 9 and compute that the section table starts at 0x14. On one file that is right; on another every section header is garbage. What is the one field you forgot to consult, and what is the correct rule?

The general habit: a format that has two variants almost always encodes the discriminator as a size. Prefer computing positions from the file's own numbers over hardcoding them, and the tool works on both variants for free.

Connect

The IMAGE_FILE_HEADER here is byte-for-byte the structure at offset 0 of a PE executable, which the PE course decodes. Reading the two side by side is the fastest way to see what "PE is COFF plus an optional header" actually means in bytes.

NumberOfSections is a 16-bit field, which is exactly why the format has a second variant, bigobj, for files that would exceed its limits. Module 1 closes with a look at that, and at the one place where a COFF field is more subtle than it appears — the section characteristics bitfield.

Next: The Section Header — ten fields, forty bytes, and a section that has no bytes in the file at all.