5.3.2 · D2Build Systems & Toolchain

Visual walkthrough — Object files — .o - .obj, symbol table, relocation entries

1,976 words9 min readBack to topic

We assume only what the parent already built: an object file holds machine code bytes plus relocation entries (notes saying "patch this slot to point at that symbol"). Everything else, we build here.


Step 1 — Memory is a numbered strip of bytes

WHAT: We draw memory as a ruler. WHY: Every symbol in the formula (, ) is going to be one address — one tick on this ruler. If you can point at ticks, you can read the formula. PICTURE: Look at the strip below. The numbers under it are addresses. There is nothing mysterious yet — just a numbered row of boxes.

Figure — Object files — .o  -  .obj, symbol table, relocation entries

Why does the linker even need to write anything? Because when the compiler made these boxes, it did not know which tick numbers they would land on. The whole .o file was written before memory positions were chosen. Relocation is the moment those positions finally exist.


Step 2 — A call instruction is opcode + a blank slot

WHAT: We zoom into main's code and find the call add instruction: one opcode box, then four blank boxes. WHY: Those four blank boxes are the placeholder the parent note kept mentioning. The relocation entry's whole purpose is to fill them. We must know exactly where they sit to measure anything. PICTURE: The orange box is the opcode 0xE8. The four red boxes are the empty displacement — currently zero, waiting to be patched.

Figure — Object files — .o  -  .obj, symbol table, relocation entries

Step 3 — The CPU computes its jump from the next instruction, not from

Here is the single fact that forces the whole formula. When this call runs, the CPU does not measure the jump from the slot. It measures from the byte right after the instruction — because by the time it reads the displacement, the "program counter" has already advanced past the entire instruction.

WHAT: We mark (first red box) and (green tick, four boxes later). WHY: The CPU's rule is fixed hardware: . We cannot change it. So our stored displacement must be measured from , not from . PICTURE: The blue arrow shows the CPU's mental model — it stands at and hops by the displacement.

Figure — Object files — .o  -  .obj, symbol table, relocation entries


Step 4 — We want to land on the symbol

WHAT: We place add somewhere further along the strip and mark its address . WHY: The entire point of the call is to land on add. So the CPU's dest must equal . That is our goal equation. PICTURE: Green box = start of add at tick . The dashed target line is where the blue arrow must land.

Figure — Object files — .o  -  .obj, symbol table, relocation entries

Set the CPU's destination equal to what we want:


Step 5 — Solve for the displacement the linker must store

WHAT: Rearrange the goal equation to isolate the number we have to write in the four red boxes. WHY: The linker's job is to fill the slot. So we solve for disp, the slot's contents.

Start from Step 4 and subtract from both sides:

Now substitute from Step 3:

PICTURE: The two arrows — one from to (the displacement itself), and the small back-hop of from to — show geometrically why a appears.

Figure — Object files — .o  -  .obj, symbol table, relocation entries

Now compare with the parent's formula: Term by term:

  • = the symbol's chosen address (Step 4),
  • = the addend, and now we see what it is: it is precisely , the width of the slot, stored as a constant in the relocation entry,
  • = the slot's address (Step 2).

Step 6 — A numeric walk-through (nail down every tick)

Let the linker choose: slot , symbol , addend .

WHAT: Plug real numbers in and check the CPU lands on . WHY: A formula you can't run on numbers is a formula you don't trust yet.

The linker writes 0x0000002C into the four red boxes. Now the CPU runs it:

PICTURE: The number line shows , the to , then the arrow landing exactly on .

Figure — Object files — .o  -  .obj, symbol table, relocation entries

Step 7 — The backward-jump case (negative displacement)

WHAT: What if add sits before main on the strip, so ? WHY: We must cover every sign. The parent's [!recall] promised the reader never hits an unshown case.

Take , , :

A negative displacement. Is that allowed? Yes — the 4-byte slot stores a signed number (two's complement), so it can encode "jump backward". The CPU check still holds:

PICTURE: The blue arrow now points left. Same formula, opposite direction — nothing special was needed.

Figure — Object files — .o  -  .obj, symbol table, relocation entries

Step 8 — The absolute case (no at all)

WHAT: Compare against the other relocation from the parent: R_X86_64_64, used when the bytes must hold the symbol's actual address, not a distance (e.g. int *p = &g;). WHY: To see why disappears here — the degenerate end of the spectrum.

An absolute reference does not measure "how far from here"; it just stores the address itself. There is no PC, no "next instruction", so there is nothing to subtract:

PICTURE: Two side-by-side strips — absolute writes the tick number straight into the boxes; PC-relative writes the gap . Same , different question.

Figure — Object files — .o  -  .obj, symbol table, relocation entries

The one-picture summary

Figure — Object files — .o  -  .obj, symbol table, relocation entries

The whole derivation on one strip: opcode 0xE8, the 4-byte slot starting at , the hop to , the target , and the displacement arrow whose length is .

Recall Feynman: the walkthrough in plain words

Picture memory as a long ruler of numbered boxes. Your call add instruction is a signpost with a blank arrow painted on it. The blank arrow lives in four boxes starting at position . Here's the catch: when the CPU reads the arrow, it's already standing four boxes past — at — because it read the whole signpost first. So if we want the arrow to point at add (position ), and the CPU always adds the arrow's number to its own standing spot, the number we paint must be " minus where the CPU stands" . Tidy that up and it's with , where the is just the width of the arrow's own boxes. If add is ahead, the arrow's number is positive; if behind, it's negative — same painting rule either way. And if the box just wants the actual address of the symbol instead of a distance (an absolute reloc), there's no "standing spot" to subtract, so you drop the and write plain .

Recall

What does stand for in ? ::: The address of the slot being patched — the first byte of the displacement field. Why is the addend for a 4-byte PC-relative field? ::: Because the CPU measures from , and negates that 4-byte gap so the arithmetic targets . Why does the absolute formula drop the ? ::: Absolute relocations store the symbol's actual address (not a distance from the patch site), so there is no "here" to subtract.

Related vault notes: PC-relative addressing in x86-64 · Linker — symbol resolution and relocation · Static vs Dynamic Linking · Compilation Pipeline — preprocess, compile, assemble, link · Sections — .text .data .bss .rodata · ELF and COFF file formats · Name mangling in C++