Every relocation is one unanswered question. "At offset 0x56 inside .text, the 4-byte value currently zero should become the address of the thing called g_data." Ten bytes, and that is the entire mechanism by which a compiler hands layout decisions to a linker.
They are also the last place in this course where a parser can be subtly, silently wrong — because the COFF specification documents a packed layout for x64 that the compiler does not actually emit, and a reader that implements the documented version gets a type of zero for every record in the file.
Three fields, ten bytes, answering three separate questions:
VirtualAddress — an offset within the section this table belongs to, not a file offset and not a virtual address. Both of those would be meaningless in a relocatable file.SymbolTableIndex — an index into the symbol table, not a name. The relocation never spells out a symbol name.Type — a machine-specific number saying what arithmetic to perform and how wide the result is.Two things follow from that, and they are the whole design. First, relocations belong to a section, so each section header points at its own table and carries its own count. Second, the type numbering is per-machine, so a reader that knows the Machine field knows which table of type numbers applies — and a reader that ignores it will decode an i386 object using the AMD64 table and produce plausible nonsense.
The .text relocation table: three records of ten bytes at file offset 0x1E8, immediately after the 108 bytes of code they describe.
000001e8: 5600 0000 1700 0000 0400 addr 0x56 sym 23 type 4 000001f2: 5d00 0000 1400 0000 0400 addr 0x5d sym 20 type 4 000001fc: 6200 0000 1500 0000 0400 addr 0x62 sym 21 type 4
And llvm-objdump -r reading the same bytes:
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000056 IMAGE_REL_AMD64_REL32 g_data
000000000000005d IMAGE_REL_AMD64_REL32 add
0000000000000062 IMAGE_REL_AMD64_REL32 call_throughCross-referencing the symbol indices against the table from the symbol concept: 23 is g_data, 20 is add, 21 is call_through. Every field agrees.
So the verified layout is the plain one:
| Offset | Size | Field | Note |
|---|---|---|---|
| 0x00 | 4 | VirtualAddress | offset within the owning section |
| 0x04 | 4 | SymbolTableIndex | low 16 bits used |
| 0x08 | 2 | Type | machine-specific numbering |
On IMAGE_FILE_MACHINE_AMD64, the types in our file are 1, 3 and 4:
| Value | Name | Meaning | Where we saw it |
|---|---|---|---|
| 0 | IMAGE_REL_AMD64_ABSOLUTE | no operation; used as padding | not present |
| 1 | IMAGE_REL_AMD64_ADDR64 | write the 8-byte absolute address | .data, 2 records |
| 2 | IMAGE_REL_AMD64_ADDR32 | write the 4-byte absolute address | not present |
| 3 | IMAGE_REL_AMD64_ADDR32NB | 4-byte address, subtract the image base | .pdata, 9 records |
| 4 | IMAGE_REL_AMD64_REL32 | 32-bit relative displacement | .text, 3 records |
The distinction that matters most is REL32 versus everything else. REL32 is a displacement: the linker computes target - (address of this field + 4) and writes that, which is exactly what a call or jmp instruction with a 32-bit relative operand needs. It is position-independent within the section, which is why code can move without being rewritten. Everything else writes a bare address, which is why it is not position-independent and why the code holding it must be rebuilt if the image moves beyond 2 GB.
The NB in ADDR32NB means "not based": the linker subtracts the image base, storing a relative address in a field that looks absolute. The .pdata records are all of this type, pointing at .text and .xdata from an exception-unwinding table — data that references code, and therefore needs to survive rebasing.
Decode all fourteen relocations in the file, then confirm against both tools. This is the table every claim in this concept rests on:
| Section | Offset | Symbol | Type | Meaning |
|---|---|---|---|---|
.text | 0x56 | 23 g_data | 4 | REL32 — the mov that loads the global |
.text | 0x5D | 20 add | 4 | REL32 — the call add |
.text | 0x62 | 21 call_through | 4 | REL32 — the call call_through |
.data | 0x08 | 12 the string literal | 1 | ADDR64 — g_ptr, an 8-byte pointer |
.data | 0x10 | 23 g_data | 1 | ADDR64 — g_addr, an 8-byte pointer |
.pdata | 0x00 | 0 .text | 3 | ADDR32NB |
.pdata | 0x04 | 0 .text | 3 | ADDR32NB |
.pdata | 0x08 | 6 .xdata | 3 | ADDR32NB |
.pdata | 0x0C | 0 .text | 3 | ADDR32NB |
.pdata | 0x10 | 0 .text | 3 | ADDR32NB |
.pdata | 0x14 | 6 .xdata | 3 | ADDR32NB |
.pdata | 0x18 | 0 .text | 3 | ADDR32NB |
.pdata | 0x1C | 0 .text | 3 | ADDR32NB |
.pdata | 0x20 | 6 .xdata | 3 | ADDR32NB |
Every row was produced by an independent parser and required to match llvm-readobj --relocations, which agrees on all fourteen offsets, symbol indices and types. The file has 1200 such records in total across the five sample objects, and all of them matched.
Read the .pdata group as a pattern: 3 RUNTIME_FUNCTION entries, each 12 bytes, each needing a .text address and a .xdata address. Two relocations per entry, nine in total because the .pdata section is 36 bytes and the last entry's second field is covered by the section's own extent. The triple of 0x00, 0x04, 0x08 is one entry; 0x0C, 0x10, 0x14 is the next; 0x18, 0x1C, 0x20 is the third.
The PE/COFF specification documents an overlapping x64 relocation layout: bits 0–11 of the first dword are the offset, bits 12–15 of that same dword are the type, and bits 16–31 of the second dword are the symbol index. Under that reading, our first record — bytes 56 00 00 00 17 00 00 00 04 00 — decodes as:
Every one of the fourteen records decodes that way to IMAGE_REL_AMD64_ABSOLUTE, symbol 0 — the padding type, referencing nothing. That is obviously wrong, and it is the signal that the reading is wrong rather than the data.
The plain reading gives offset 0x56, symbol 23, type 4 — which matches llvm-objdump exactly, including the REL32 that a call instruction obviously requires. clang emits the plain layout, and so does every object in this course.
This is recorded as an observed discrepancy, not as a claim that the specification is wrong. Some documentation and some linkers do describe the packed form, and a tool that has to read any COFF file should know both exist. But a reader built only to the documented variant will report fourteen zero relocations in this object, and nothing in the file will tell it that something is off.
$ clang --target=x86_64-pc-windows-msvc -c sample.c -o sample_msvc.obj
$ llvm-objdump -r sample_msvc.obj
$ xxd -s 0x1e8 -l 32 sample_msvc.obj
$ llvm-readobj --relocations sample_msvc.obj | head -20Do both decodings and watch one of them fail. This is a five-minute experiment that is worth more than the table above:
# The reading that FAILS on real files (documented, not emitted):
type = (dword0 >> 12) & 0xF
offset = dword0 & 0xFFF
symbol = (dword1 >> 16) & 0xFFFF
# -> type 0 (ABSOLUTE) and symbol 0, for every record
# The reading that matches the bytes:
offset = dword0
symbol = dword1 & 0xFFFF
type = word2
# -> 0x56/23/4, 0x5d/20/4, 0x62/21/4Then confirm that the type numbering really is per-machine, by looking at the same source compiled for i386:
$ clang --target=i686-pc-windows-msvc -c sample.c -o sample_m32.obj
$ llvm-objdump -r sample_m32.obj | head -6
RELOCATION RECORDS FOR [.text]:
0000000000000057 IMAGE_REL_I386_DIR32 _g_data
000000000000005d IMAGE_REL_I386_DIR32 _add
0000000000000069 IMAGE_REL_I386_REL32 _call_throughDifferent type names and different numbers for the same source constructs, and a leading underscore on every symbol because i386 uses the cdecl decoration that x64 dropped. The container is byte-identical in layout; Machine is what tells you which dialect of the type table applies.
Without looking back: a .text relocation record reads addr 0x5D, symbol 20, type 4. Which of the three fields is a section-relative offset rather than a file offset or a virtual address, and what does type 4 mean the linker will do?
You ship a COFF-reading library used on both x64 and ARM64 builds. It decodes relocation types with a single hard-coded table of AMD64 numbers. On the ARM64 objects it reports plausible types and produces a linked image with silently wrong pointers. What is the actual defect, and what is the minimum correct design?
The general habit: a numeric code inside a file is only meaningful relative to a declared context. If the file says what it is, read that first — and if you meet a code you have no table for, say so instead of falling back to a nearby one.
This is the ELF relocation table with a different container, and the comparison is instructive. ELF keeps relocations in a section (.rela.text), typed by a section index, and the symbol table gives the name. COFF keeps them in a side table that the section header points at, typed by a machine-specific number, and the symbol table gives an index. Same job, and ELF's version is easier to extend — a new relocation type is a new section — while COFF's is a new number in a table that has to be kept in sync with every machine.
The REL32 idea is the same one the ELF memory mapping concept relies on: code that stores a displacement rather than an address can be placed anywhere. PE base relocations exist precisely because some data cannot be relative, and ADDR32NB is the COFF-object half of that same problem.
One mechanism remains, and it is the one that makes C++ linking work at all.
Next: COMDAT and Duplicate Sections — how a linker is told which of two identical sections to keep.