Everything so far in this course has been about a file: its header, its section table, its symbol table, its relocations. A COFF object is a well-specified container that no program runs. The whole reason it exists is that something comes along afterwards and destroys the information you have spent three modules reading.
That something is the linker. It takes objects that each have zero real addresses, assigns addresses, applies relocations so those addresses appear in the bytes, resolves which copy of a duplicated symbol wins, and throws away the sections that only mattered during compilation. After it runs, the IMAGE_REL_I386_REL32 entry that said "at offset 14, add the distance to _helper" is gone, and in its place is a number.
This concept runs a real link and watches each of those happen, because a COFF course that stops at the object file has described a format and not a process. Everything here is a comparison of the same bytes before and after.
An object file, in the terms this course has used: sections whose VirtualAddress is zero, a symbol table whose Value is a section-relative offset, and a set of fixups saying which four bytes to overwrite and how. The linker does five things, and each is a separate transformation:
| Step | Changes | Observable as |
|---|---|---|
| 1. Place | Every section gets a real VirtualAddress | The section table's VirtualAddress fields stop being zero |
| 2. Absorb | Sections with the same name from different objects are concatenated | One output section is larger than any input section |
| 3. Relocate | Each fixup's target is computed and written into the section's bytes | Zero bytes in the object are non-zero in the image |
| 4. Resolve | Duplicate COMDAT sections are eliminated, one kept | The output is smaller than the sum of the inputs |
| 5. Discard | Sections flagged removable or discardable are dropped | Input sections have no output counterpart |
Two properties of this list are worth noticing before the evidence. Steps 1 and 3 are inseparable in practice: you cannot know what to write into a relocation until you know where the sections are, and you cannot know where the sections are until you decide the layout. And step 5 is not a cleanup pass at the end — it happens before placement, because a discarded section must not consume address space. A linker that discards after placing wastes the space, and a map file that lists discards before placements is showing you the real order of operations.
Two source files, compiled to COFF objects. Every function in both has a single-expression body, so each one is written on one line; that is why the source is so short and why the machine code is so regular.
| File | Contents |
|---|---|
link_prog.c | Declares helper, then entry which returns helper(x) + 1, then main which returns entry(5) |
link_lib.c | lib_add which returns a + b, and an initialised global lib_val = 11 |
$ clang --target=i686-pc-windows-msvc -c link_prog.c -o link_prog.obj
$ clang --target=i686-pc-windows-msvc -c link_lib.c -o link_lib.objThat gives three call sites and one data initialiser, which is exactly enough to see placement, absorption, relocation and discarding all happen in one link.
The object carries two fixups, both IMAGE_REL_I386_REL32:
$ llvm-readobj --relocations link_prog.obj
Section (1) .text
0xE IMAGE_REL_I386_REL32 _helper (12)
0x45 IMAGE_REL_I386_REL32 _entry (11)The tool wraps that in a Relocations [ ] block, and it does the same for every object in the link — which is worth knowing, because a relocation list that names its own section tells you that a fixup in one section must be applied before the addresses of the next section are final.
Every section's VirtualAddress is 0x0. That is not a placeholder the linker will overwrite later — it is the truth: the code is not at any address, so it has no address. And the four bytes at offset 0xe of .text are 00 00 00 00. The relocation is not a note next to the data; the relocation is the data, and the zeros are its placeholder.
REL32 and not a plain address.IMAGE_REL_I386_REL32 does not write an absolute address. It writes the difference between two addresses, which means the instruction it fixes stays correct no matter where either side is loaded. That is the same reason a call instruction has a relative displacement: the CPU computes next_address + displacement. The COFF fixup is the assembler and linker cooperating to keep that property intact — the object stores the displacement, and the linker resolves it to a displacement, never to an address.$ ld --oformat pei-i386 -m i386pe link_prog.obj link_lib.obj \
-o link_prog.exe --entry entry
$ file link_prog.exe
link_prog.exe: PE32 executable for MS Windows 4.00 (console), Intel i386, 4 sectionsThe addresses are real now:
Machine: IMAGE_FILE_MACHINE_I386 (0x14C) ImageBase: 0x400000 AddressOfEntryPoint: 0x1000 SectionAlignment: 4096 FileAlignment: 512 .text VirtualAddress 0x1000 VirtualSize 0x61 .data VirtualAddress 0x2000 VirtualSize 0x4 .rdata VirtualAddress 0x3000 VirtualSize 0x10 .idata VirtualAddress (import directory)
Two things happened at once. The sections were given addresses on a 4096-byte grid, so .text is at 0x1000 and .data at 0x2000 even though .text is only 0x61 bytes — the gap exists so that a page can be made writable without making the code page writable. And the ImageBase of 0x400000 was chosen, which is the preferred base for 32-bit Windows executables. The virtual address 0x1000 is relative; the address the program actually runs at is 0x400000 + 0x1000 = 0x401000.
So there are three numbers and they are easy to conflate. The section's VirtualAddress is relative to the image base. The ImageBase is the image's preferred load address. The actual address is their sum, and at run time it may be a different sum entirely — which is the entire reason base relocations exist, and something this linker will not emit, so it is taught in the PE course rather than here.
Now the proof. Disassemble the linked image:
00401000 <_entry>: 401000: 55 pushl %ebp 401001: 89 e5 movl %esp, %ebp 401003: 50 pushl %eax 401004: 8b 45 08 movl 0x8(%ebp), %eax 401007: 8b 45 08 movl 0x8(%ebp), %eax 40100a: 89 04 24 movl %eax, (%esp) 40100d: e8 0e 00 00 00 calll 0x401020 <_helper> 401012: 83 c0 01 addl $0x1, %eax 401015: 83 c4 04 addl $0x4, %esp 401018: 5d popl %ebp 401019: c3 retl
Compare the e8 opcode's four operand bytes with the zeros in the object:
| Where | Bytes at the fixup | Meaning |
|---|---|---|
link_prog.obj, .text + 0xe | 00 00 00 00 | Nothing. The code is nowhere |
link_prog.exe, 0x40100d | 0e 00 00 00 | 0x0e = 14 |
And 14 is exactly right, and you can check it yourself. The call instruction begins at 0x40100d and is five bytes long, so the address of the next instruction is 0x401012. The target is _helper at 0x401020. The displacement is 0x401020 - 0x401012 = 0x0e. The CPU will do that addition itself at run time and arrive at 0x401020, without ever knowing that 0x400000 exists.
That subtraction is the entire relocation. Four bytes changed, and the arithmetic that produced them is a two-term difference of addresses — the same shape as every other relative fixup in every object format. The relocator's real job is not arithmetic; it is knowing which two addresses to subtract.
The object had a .debug$S section and an address-significance section. Neither appears in the image's four sections. Here is the entire evidence, from the linker's own map file:
Discarded input sections
.debug$S 0x00000000 0x5c link_prog.obj
.llvm_addrsig 0x00000000 0x2 link_prog.obj
.debug$S 0x00000000 0x5c link_lib.obj
.llvm_addrsig 0x00000000 0x0 link_lib.objThat is step 5 observed, and it closes a loop from Module 1. The characteristics flags you decoded there are not descriptive — they are instructions to the linker. Reading them back out of the object, with the parser shipped with this course:
link_prog.obj
sec4 .debug$S rawsize=92 char=0x42300040
IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_DISCARDABLE | IMAGE_SCN_MEM_READ
sec5 /4 rawsize=2 char=0x00100800
IMAGE_SCN_LNK_REMOVE
Two different instructions, and the difference is worth holding onto. MEM_DISCARDABLE says "this section's contents may be thrown away — it exists for a build-time consumer, not for the loader". LNK_REMOVE says the stronger thing: "this section is not part of the program at all, and any content it happens to carry is padding". The compiler sets the first on debug data and the second on the address-significance section, and the linker honours both by not placing either.
And a small observation about that /4. The section's name in the header is not .llvm_addrsig — it is the four characters /4, because the name is too long for the eight-byte field and /N means "the real name is in the string table at offset N". This is the string-table indirection from Module 1, doing its job in a place where a reader is not expecting it. The map file prints the resolved name; the object stores the reference. Which means the string table is load-bearing for the linker, not just for your own decoder — a program that printed section names from the object without resolving them would show you /4 and not the section.
Read the discard sizes as well. .debug$S is 0x5c = 92 bytes in both objects — the same 92 bytes, discarded twice, because both were compiled with the same flags. .llvm_addrsig is 0x2 bytes in one object and 0x0 in the other: the second object had an address-significance section of length zero, and a zero-length section is still a section that gets discarded. If you are ever unsure whether a section participated in a link, this list is the authority; the output section table only shows what survived.
Doing step 3 yourself, for the second relocation, to confirm the model transfers. The object says 0x45 IMAGE_REL_I386_REL32 _entry, so offset 0x45 of .text holds a displacement to _entry. In the image, .text starts at 0x401000, _entry is at 0x401000, and the only call to it is from main:
And -0x49 is ffffffb7 in the image. Note the + 4 in the last line: the displacement is measured from the end of the field, not its start, because that is where the CPU resumes after reading the four bytes. Getting that wrong by four produces a link error at build time or a jump to a misaligned address at run time, and it is the single most common bug in a hand-written relocator.
So the whole relocation algorithm, for the REL32 family, is:
VirtualAddress and the input section's offset within it.Value in the object becomes image_base + section.VirtualAddress + Value in the image.Step 4 is the interesting one for a hand-rolled linker and step 3 is where bugs live. Sign extension is the trap: a displacement of -0x49 must be stored as ffffffb7, and storing 0x000000b7 turns a backward call into a jump 184 million bytes forward. The field is four bytes wide, not four bytes of magnitude, and the value is a difference, so it is signed by nature. Every relocation type in this table that ends in a two-digit number is a signed displacement of some width, and the number in the name is that width.
$ ld --oformat pei-i386 -m i386pe link_prog.obj link_lib.obj \
-o link_prog.exe --entry entry -Map link_prog.map
$ llvm-objdump -d --section=.text link_prog.exe
$ llvm-readobj --sections link_prog.exe | head -40link_lib.c, relink, and diff the map. The new function changes .text's size, which can change nothing else — but if you add a variable, .data grows and .rdata may move. Watching the grid spacing hold at 4096 while contents change is the clearest demonstration of why alignment is not rounding but allocation.VirtualAddresses stay the same and the ImageBase changes, so the fixup displacements should be unchanged — because they are differences. Any that change are absolute fixups, and finding one is the payoff of the whole relative-vs-absolute distinction.helper and relink. Read the error message, and note that it is an unresolved external rather than a bad relocation. A missing symbol and a miscomputed displacement are different failures with different evidence, and knowing which one you have saves a long debugging session..debug$S out of the object and check for IMAGE_SCN_MEM_DISCARDABLE; read the /4 section and check for IMAGE_SCN_LNK_REMOVE; then read .text and check that it has neither. Three data points, one rule, and the rule is now observed rather than quoted.And a self-check: the displacement you computed by hand was -0x49, and the disassembly should contain e8 b7 ff ff ff. If your linked binary shows a forward call from main to entry, your linker placed main before entry and the sign is wrong — go back to the + 4.
Without looking back: a COFF object has .text with VirtualAddress = 0, four zero bytes at offset 0xe, and a relocation 0xE IMAGE_REL_I386_REL32 _helper. After linking, .text is at 0x1000, the image base is 0x400000, and _helper is at 0x401020. What goes into those four bytes, and what was actually true about the zeros before the link?
You are writing a COFF linker — the object side of it — and it works for most inputs. It fails on exactly one class: any program that calls a function defined in a file that sorts later in the link order. Forward calls are fine; backward calls resolve to a huge positive number and the program jumps into the middle of nowhere. Relocations report no errors, the image is well-formed, and readelf-equivalent tools show the fixups as applied. What is wrong, and how would you confirm it in one run?
The general habit: a bug that only appears for one sign of a value is almost always a sign-handling bug. Read the failing value in hex and ask what the correct value would be; if the two differ only in their high bits, you have found it.
Step 3 is the ELF course's relocation machinery, running on a different container. ELF relocations use a table of records with explicit symbol and addend fields; COFF's IMAGE_REL_I386_REL32 is a single 16-bit number and the target is implied by the field. Same arithmetic, much less explicit, and the practical consequence is that a COFF relocation cannot express "this symbol plus this addend" — the addend is whatever is already in the field. That difference in expressiveness is why COFF objects are smaller and why the two formats' relocation tables do not convert cleanly.
Step 1 introduces the ImageBase, and the fact that the virtual addresses are relative to it is the setup for the PE course's base relocation concept. When the loader cannot put the image at its preferred base, every absolute address baked into the image is wrong, and the base relocation table is the list of places to patch. This concept stopped short of that deliberately: the linker used here will not emit one, and a claim that could not be checked against a file is not taught here.
Steps 2 and 5 both point at the next concept, which is the artifact that records all five steps in the order they happen — and names, in plain text, every input section that was discarded and why.
Next: Map Files.