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.
Compiling and linking are two different jobs, and the boundary between them is what COFF is built around:
Those notes are relocations. They are the entire reason this file format exists, and the entire reason the file has no addresses in it.
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 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 offset | Size | What it is |
|---|---|---|
| 0x00 | 20 | File header. 7 fields. No magic number. |
| 0x14 | 9 × 40 = 360 | Section table. One 40-byte record per section. |
| 0x17C | 108 | .text raw bytes — the actual machine code |
| 0x1E8 | 30 | .text relocations: 3 records of 10 bytes |
| 0x206 | 48 | .data raw bytes |
| 0x236 | 20 | .data relocations: 2 records |
| 0x348 | 31 × 18 = 558 | Symbol table |
| 0x576 | 57 | String 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.
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:
SizeOfOptionalHeader non-zero. Has addresses, an entry point, data directories. The OS can run it.SizeOfOptionalHeader zero. Has relocations instead. Only a linker can consume it.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.
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_*.objWhat 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.
Without looking back: you are handed a file whose SizeOfOptionalHeader is zero. Can the operating system run 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.
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.