Back to course

The Link

23 minintermediateModule 4: The LinkContinues from coff-line-numbers
23 min · Module 4: The Link · Algorithms

Why This Matters

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.

A Simple Model

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:

StepChangesObservable as
1. PlaceEvery section gets a real VirtualAddressThe section table's VirtualAddress fields stop being zero
2. AbsorbSections with the same name from different objects are concatenatedOne output section is larger than any input section
3. RelocateEach fixup's target is computed and written into the section's bytesZero bytes in the object are non-zero in the image
4. ResolveDuplicate COMDAT sections are eliminated, one keptThe output is smaller than the sum of the inputs
5. DiscardSections flagged removable or discardable are droppedInput 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.

The Actual Detail

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.

FileContents
link_prog.cDeclares helper, then entry which returns helper(x) + 1, then main which returns entry(5)
link_lib.clib_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.obj

That gives three call sites and one data initialiser, which is exactly enough to see placement, absorption, relocation and discarding all happen in one link.

Step 3, in isolation: the relocations before

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.

Why 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.

Steps 1 and 2 together: the image

$ 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 sections

The 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.

Step 3, in isolation: the relocations after

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:

WhereBytes at the fixupMeaning
link_prog.obj, .text + 0xe00 00 00 00Nothing. The code is nowhere
link_prog.exe, 0x40100d0e 00 00 000x0e = 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.

Step 5: the sections that did not survive

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.obj

That 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.

A Real Example

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:

fixup_offset = 0x45 field_address = image_base + .text.VirtualAddress + fixup_offset = 0x400000 + 0x1000 + 0x45 = 0x401045 target_address = image_base + .text.VirtualAddress + _entry.Value = 0x400000 + 0x1000 + 0x0 = 0x401000 displacement = target_address - (field_address + 4) = 0x401000 - 0x401049 = -0x49

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:

  1. Find the field: the input section's raw data at the fixup offset.
  2. Work out where that field will end up, from the output section's VirtualAddress and the input section's offset within it.
  3. Resolve the symbol: a section-relative Value in the object becomes image_base + section.VirtualAddress + Value in the image.
  4. Subtract, and write the result in the field's width, sign-extended from the difference — not truncated.

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.

Try It Yourself

$ 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 -40

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.

Check Your Understanding

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?

Apply It

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.

Connect

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.