5.3.2 · D3Build Systems & Toolchain

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

2,469 words11 min readBack to topic

You have met the relocation formula on the parent note:

This page does one thing: it throws every kind of case at that formula so you never meet a scenario in the wild you haven't already solved on paper.


The scenario matrix

Think of a relocation as having a few "knobs". Each knob has a small set of settings. The matrix below lists every setting we must cover, and which worked example nails it.

# Case class The knob's settings Covered by
A Reloc kind absolute () vs PC-relative () Ex 1, Ex 2
B Displacement sign forward jump () vs backward jump () Ex 2, Ex 3
C Addend zero addend vs non-zero () Ex 1, Ex 2
D Symbol binding DEFINED here vs UND (undefined, needs another .o) Ex 1, Ex 4
E Degenerate distance (patch points at itself) Ex 5
F Zero-size input .bss symbol — takes no file bytes Ex 6
G Overflow / limit displacement doesn't fit in 32 bits Ex 7
H Word problem real linker command, real readelf reading Ex 8
I Exam twist two definitions of one symbol (multiple-definition) Ex 9

For the picture-based examples we'll use one number line: memory addresses increasing to the right. See it once:


Ex 1 — Absolute reloc, zero addend (cells A, C, D-defined)

Forecast: guess before reading — will the bytes be g's address, or a distance? (Absolute → the address itself.)

  1. Identify the knobs. Why this step? Every relocation is solved by first reading , , off the entry. Here , , and because it's absolute we don't need .
  2. Pick the formula: absolute ⇒ . Why this step? The bytes of p must hold the actual address of g so the running program can dereference it. A distance would be meaningless for a data pointer.
  3. Compute: . Why this step? Just the arithmetic; the addend is 0 because &g has no +offset.

Verify: . Plugging back, ✓. Units: an address, correct for a pointer.


Ex 2 — PC-relative forward call, addend (cells A, B-forward, C-nonzero, D-UND)

Forecast: add is ahead of the call — so is the displacement positive or negative?

  1. Read knobs: , , . Why this step? PC-relative needs all three; is the slot itself, not the next instruction.
  2. Formula: PC-relative ⇒ . Why this step? The CPU computes the branch target as (address of next instruction) displacement. The addend soaks up the 4 displacement bytes between and that next instruction, so the jump lands exactly on add.
  3. Compute in decimal: , . . Why this step? , a small positive displacement — forward, matching our forecast.

Verify: next-instruction address . Target ✓. Lands exactly on add.


Ex 3 — PC-relative backward call (cell B-backward)

Forecast: target is below in memory → expect a negative displacement.

  1. Knobs: , , . Why this step? Same PC-relative case; only the numbers moved.
  2. . Why this step? Negative, as forecast — the CPU jumps backwards.
  3. Interpret the bytes: the field is a signed 32-bit integer; is stored two's-complement as 0xFFFFFEFC. Why this step? Machine code never stores a minus sign; negativity lives in the top bit.

Verify: next instruction . Target ✓.


Ex 4 — Undefined symbol that is never resolved (cell D-UND, error path)

Forecast: does this fail at compile or at link time?

  1. Compiling main.c alone succeedsadd was declared (extern), so the compiler is happy. Why this step? The compiler only checks types & declarations, not existence of the definition.
  2. The .o carries a symbol-table entry add marked UND, plus a relocation waiting for its address. Why this step? This is the "I am looking for add" badge from the parent note.
  3. The linker scans every input for a DEFINED add. Finding none, it cannot compute , so it cannot apply . Why this step? No ⇒ no arithmetic possible ⇒ hard stop.
  4. Result: undefined reference to 'add'. Why this step? This is a link error; adding a #include would not help — you must supply the defining .o/library. See Linker — symbol resolution and relocation.

Verify: the fix is gcc main.o add.o. Conceptually: number of unresolved UND symbols must reach before an executable is produced. Here it was , so failure. ✓ (see Static vs Dynamic Linking for how a shared lib supplies later instead.)


Ex 5 — Degenerate case: symbol patches itself, (cell E)

Forecast: if "here" equals "there", is the displacement zero?

  1. Knobs: , . Why this step? Set up the degenerate values.
  2. . Why this step? The and cancel; only the addend survives.
  3. Read it: means "jump to 4 bytes before the next instruction" — i.e. back to the start of this instruction's operand. Why this step? Shows the addend is doing real geometric work even when ; it's never just decoration.

Verify: next instruction . Target ✓. Even in the degenerate case the formula lands on exactly.


Ex 6 — Zero-size file input: .bss (cell F)

Forecast: guess the on-disk byte count for a million ints.

  1. big is uninitialised ⇒ goes to .bss. Why this step? .bss is the zero-init section; it stores a size, not the zeros themselves (see Sections — .text .data .bss .rodata).
  2. File cost bytes. The symbol records size bytes. Why this step? The loader zero-fills at run time, so the disk image stays tiny.
  3. Any pointer to big still needs an absolute reloc () once the loader assigns its address. Why this step? Zero file bytes doesn't mean zero relocations — the address is still unknown at compile time.

Verify: if this were .data, file would grow by bytes. In .bss it grows by . Savings bytes ✓.


Ex 7 — Limiting case: displacement overflow (cell G)

Forecast: does a 3-gigabyte forward jump fit in 32 signed bits?

  1. Max positive displacement . Why this step? Establishes the ceiling of a signed 32-bit slot.
  2. Required . Why this step? Compare need vs capacity — the number cannot be represented.
  3. The linker emits relocation truncated to fit: R_X86_64_PC32. Why this step? Writing the number would silently corrupt the top bits, so the linker refuses. Fix: compile with a larger code model (or use R_X86_64_64 absolute / a GOT/PLT indirection).

Verify: ⇒ overflow ✓. And , so a 64-bit absolute reloc would fit. ✓


Ex 8 — Word problem: reading a real readelf -r line (cell H)

Forecast: which of Ex 1–7 does this reduce to?

  1. Compute the slot's final address : base offset . Why this step? The reloc's Offset is within the section; needs the section's final placement added.
  2. Read (from add - 4), . Why this step? These are the other two knobs, straight off the line.
  3. It's R_X86_64_PLT32 ⇒ PC-relative ⇒ . Why this step? Same case as Ex 2, now driven by real tool output.
  4. Compute: , . . Why this step? Final answer, the byte value (little-endian: 2B 00 00 00).

Verify: next instr ; target ✓.


Ex 9 — Exam twist: two definitions of one symbol (cell I)

Forecast: two globals, same name — merge, error, or silent bug?

  1. Each compiler sees only its own file, so each emits a GLOBAL tentative definition symbol x. Neither compilation fails. Why this step? The compiler has no cross-file view; uniqueness of globals is the linker's job (see Name mangling in C++ for how C++ makes symbol names unique, unlike C).
  2. At link, the linker finds two DEFINED x with GLOBAL binding. Why this step? Each "I can provide x" badge must be unique; two providers is a conflict.
  3. Outcome depends on the era: modern GCC (-fno-common) ⇒ multiple definition of 'x' error; legacy common-symbol behaviour ⇒ silently merges into one x (a classic aliasing bug). Why this step? Shows both the safe error and the dangerous silent path.
  4. Fix: static int x; (file-local, becomes a LOCAL symbol) or one definition int x; plus extern int x; elsewhere. Why this step? Restores the "exactly one provider" invariant.

Verify: count of GLOBAL DEFINED x symbols must be for a clean link. Two files ⇒ count ⇒ conflict ✓.


Recall Which formula for each reloc kind?

Absolute (R_X86_64_64) uses ::: PC-relative (R_X86_64_PC32 / PLT32) uses ::: Why the in PC-relative? ::: the CPU adds the stored value to the next instruction's address, so the linker must store a distance from here, not an absolute address.

Recall Sign & overflow sanity

A forward jump gives displacement of what sign? ::: positive A backward jump gives what sign? ::: negative A 4-byte PC32 field holds what range? ::: signed 32-bit, ; beyond it the linker says "relocation truncated to fit"