Back to course

Why COFF Exists

14 minbeginnerModule 1: The Relocatable Object
14 min · Module 1: The Relocatable Object · Orientation

Why This Matters

You have written int add(int a, int b) and called it from another file, and it worked. Something had to work out what the address of add actually is — because the compiler that compiled the caller did not know where the function would end up, and neither did the compiler that compiled it.

COFF is the container that lets a compiler hand the linker a half-finished answer. It is the format of the .obj file on Windows and of the object files the GNU toolchain produces for Windows targets. It is also, deliberately, not a format you can run.

You already know its sibling. The PE course covers the COFF header as it appears inside a PE executable. This course covers the other half: the file that exists before the link, where addresses are still unknown and a table of "fix these up" is the whole point.

A Simple Model

Compiling and linking are two different jobs, and the boundary between them is what COFF is built around:

  1. The compiler turns one source file into one object. It can resolve everything inside that file, because it can see all of it. It cannot resolve anything across files, because the other file has not been compiled yet, and even if it had, the answer depends on the order things get placed in memory.
  2. So it writes down the question instead of the answer. Wherever the code contains something whose value depends on layout, the compiler leaves a zero and a note: "at this offset, put the address of that symbol here."
  3. The linker concatenates the objects, decides where everything goes, and then answers every note.

Those notes are relocations. They are the entire reason this file format exists, and the entire reason the file has no addresses in it.

The simplification is the point, not a flaw. "No addresses at all" is close to true and worth stating sharply: in every section of every object in this course, VirtualAddress is exactly zero. The one qualification is that a symbol's offset within its own section is known and stored — a function at offset 80 of .text is a fact the compiler can record, because the section is its own private space for now. It is only the section's own eventual address that is unknown.

The Actual Detail

The sample used throughout this course is one small C file, compiled to a real COFF object:

int add(int a, int b) { return a + b; }

int g_data = 42;
int g_bss;
static int s_data = 7;
const char g_str[] = "coff";
const char *g_ptr = "ptr";
int *g_addr = &g_data;

int table[4] = {1, 2, 3, 4};

int call_through(int (*fn)(int, int), int v) { return fn(v, v); }
int use(void) { return call_through(add, g_data); }

That is 21 lines, and it produces a 1455-byte object. Almost all of the interest is in the parts that are not code: the file header says there is no optional header at all, the section table says every address is zero, and three relocation records sit in a side table saying where the linker owes answers.

Here is the whole file, laid out by region. Sizes and offsets are read out of the file itself:

File offsetSizeWhat it is
0x0020File header. 7 fields. No magic number.
0x149 × 40 = 360Section table. One 40-byte record per section.
0x17C108.text raw bytes — the actual machine code
0x1E830.text relocations: 3 records of 10 bytes
0x20648.data raw bytes
0x23620.data relocations: 2 records
0x34831 × 18 = 558Symbol table
0x57657String table, running to the last byte of the file

Two things about that table are worth pausing on, because neither is how ELF is arranged.

Relocations do not sit next to their code. The three .text records are at 0x1E8, immediately after the 108 bytes of .text that they describe — but that is a coincidence of this file's layout, not a rule. Every section has its own PointerToRelocations, and the linker seeks to each independently. A section's relocations can be anywhere in the file, or immediately before the code, or at the very end.

There is no magic number. A COFF object begins straight into the header, and there is nothing to identify it. The file utility reports x86-64 COFF object file by inferring from the plausible field values, not by matching a signature. This is a real weakness: a file with garbage in its first twenty bytes is not rejected, it is misinterpreted. Contrast the ELF magic 7f 45 4c 46, which you met in ELF identification.

A Real Example

Here is the payoff, in the first twenty bytes of the real object. There is no magic number, no version, no class byte — just seven fields:

00000000: 6486 0900 2aa5 b66a 4803 0000 1f00 0000  d...*..jH.......
00000010: 0000 0000 2e74 6578 7400 0000 0000 0000  .....text.......
00000020: 0000 0000 6c00 0000 7c01 0000 e801 0000  ....l...|.......
00000030: 0000 0000 0300 0000 2000 5060 2e64 6174  ........ .P`.dat

Two of those bytes do more work than all the others together. Offset 0x10 and 0x11 are 00 00: SizeOfOptionalHeader = 0. In a PE executable that field holds 224 or 240, because an executable needs a place to record its entry point, its image base, and sixteen data directories. An object needs none of those things, so the field is zero and the optional header simply is not there.

That single value is the cleanest possible definition of the difference between the two formats:

Both carry the identical 20-byte IMAGE_FILE_HEADER at offset 0. That is the whole relationship between them: PE is a COFF header plus an optional header plus more. The PE course starts where this one stops.

Also note the third line: byte 0x14 is 2e, a full stop. That is not part of the header — the header ended at 0x13. It is the first byte of the first section's name, .text, which is how we know the section table begins at 0x14 and not somewhere else.

Try It Yourself

Build the reference object. This is the exact command behind every byte in this course:

$ clang --target=x86_64-pc-windows-msvc -c sample.c -o sample_msvc.obj
$ file sample_msvc.obj
sample_msvc.obj: x86-64 COFF object file, not stripped, 9 sections,
                 symbol offset=0x348, 31 symbols, ..., 1st section name ".text"

Then produce two more dialects of the same source, and watch how little changes:

$ clang --target=x86_64-w64-windows-gnu -c sample.c -o sample_gnu.obj
$ clang --target=i686-pc-windows-msvc     -c sample.c -o sample_m32.obj
$ file sample_*.obj

What to look for: the GNU build has 8 sections, not 9, and the i386 build reports symbol offset=0x254 with 27 symbols. Same source, three objects, three different answers. The container is identical in all three; the content is a dialect. Module 1 and Module 2 are about the container.

For the tools used throughout: the full LLVM suite sits beside clang, so llvm-readobj and llvm-objdump are usually available once you add that directory to your PATH.

Check Your Understanding

Without looking back: you are handed a file whose SizeOfOptionalHeader is zero. Can the operating system run it?

Apply It

A colleague hands you a COFF object and says "it crashed". You spend ten minutes looking for the bug in the C code before you realise what is wrong with their diagnosis. What is the actual problem with debugging a relocatable object?

The general habit: before debugging a binary artifact, check which stage of the toolchain produced it. A file that is "supposed to run" but does not is very often a file that was never meant to.

Connect

The closest thing to this in the ELF course is the relationship between a relocatable object and a linked executable — but the containers are more different than ELF's are. An ELF object keeps relocations in a section; a COFF object keeps them in a side table that the section header points at, one table per section. The idea is identical, the layout is not.

You now know what the file is for. The next three concepts take it apart: the 20-byte header, the 40-byte section header, and the characteristics bitfield that is more subtle than it looks.

Next: The File Header — all seven fields, and the machine types that tell you how wide the pointers are.