5.3.2 · D4Build Systems & Toolchain

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

2,468 words11 min readBack to topic

This page tests everything in the parent note. Try each problem before opening its solution. Every symbol used here — , , , UND, .bss, relocation types — is re-explained the first time it appears so you never need to scroll back.

Before we begin, a one-line refresher of the two patch formulas, because almost every problem below uses them:

The mnemonic is ==S A P==: Symbol plus Addend, minus P only when the reference is PC-relative.


Level 1 — Recognition

L1.1 — Read the nm letters

nm prints one letter per symbol. What does each of these mean, and is the symbol defined here or needed from elsewhere? T, t, D, B, U

Recall Solution

A symbol is just a named thing — a function or a global variable. The letter tells you which section it lives in and whether this file provides it.

  • T — in .text (code), global, defined here.
  • t — in .text, local (lowercase = local, e.g. a static function), defined here.
  • D — in .data (an initialised global), defined here.
  • B — in .bss (a zero-initialised global), defined here.
  • Uundefined: this file uses the name but does not provide it. The linker must find it in another .o or library.

Rule of thumb: ==capital = global, lowercase = local, U = "I'm looking for this."==

L1.2 — Which section?

Place each declaration into .text, .data, .bss, or .rodata: int a = 7; · int b; · const char *msg = "hi"; (the literal "hi") · void f(){}

Recall Solution
  • int a = 7;.data. It is a global with a nonzero initial value, so the real bytes 07 00 00 00 must be stored in the file.
  • int b;.bss. A global with no initialiser starts at zero; .bss records only a size, not bytes, so it costs no file space.
  • the string literal "hi".rodata (read-only constants). The pointer msg itself goes to .data.
  • void f(){}.text (compiled machine code).

Level 2 — Application

L2.1 — Absolute relocation arithmetic

A global pointer p (8 bytes in .data) must hold &g. The linker finally places g at address 0x404050. The relocation is R_X86_64_64 (absolute, 64-bit), addend . What bytes get written into p?

Recall Solution

Absolute formula: . Here , , so the value is Those 8 bytes are literally the address of g. No subtraction of — an absolute reloc does not care where the slot is, only what address to write.

L2.2 — PC-relative call displacement

main contains call add. The 4-byte displacement slot sits at . The linker places add at . The reloc is R_X86_64_PLT32 with addend . What 32-bit displacement is written?

Recall Solution

PC-relative formula: . Compute in decimal to be safe: , . So the displacement is , written little-endian as 34 00 00 00.

Sanity check (why this is right): the CPU computes the destination as (address of the next instruction) + displacement. The next instruction is . Then . ✔ It lands exactly on add.

L2.3 — The role of the addend

Repeat L2.2 but with (a student "forgot" the addend). Where does the call now land, and by how many bytes is it wrong?

Recall Solution

With : displacement . Destination . The real target is 0x1040, so we land 4 bytes too far. That off-by-4 is exactly the width of the displacement field — which is why the assembler bakes in to cancel it.


Level 3 — Analysis

You compile two files. math.c defines int add(int,int){...}. main.c has only extern int add(int,int); and calls it. You run gcc main.c -o app and get undefined reference to 'add'. The header was included. What is wrong, and what is the fix?

Recall Solution

In main.o, add is a symbol marked U (undefined) — main uses it but no .o on the link line defines it. You never gave the linker math.o. undefined reference is a linker error, not a compiler error: the declaration (from the header) satisfied the compiler; the definition (the actual bytes of add) is what's missing. Fix: put the defining file on the link line: gcc main.c math.c -o app. Adding another #include does nothing — a header supplies declarations, not definitions. See Linker — symbol resolution and relocation for how matching U to a definition works.

L3.2 — Trace the relocation across two files

main.o has: main = T at .text+0, add = U, plus one R_X86_64_PLT32 reloc at .text offset 0x1, symbol add, addend -4. add.o has: add = T at .text+0. After linking, .text is laid out as: main at 0x1000, then add at 0x1030. The displacement slot is at file layout offset main+0x1. Compute the patched displacement.

Recall Solution

First find the addresses:

  • = address of the slot = main base + reloc offset = .
  • = address of add = .
  • .

PC-relative: . Written little-endian: 2B 00 00 00. Verify: next instruction is at ; . ✔

L3.3 — Why two int x; explode

Both a.c and b.c contain the file-scope line int x; (no static, no extern). Explain, in symbol-table terms, what goes wrong at link time and give two correct fixes.

Recall Solution

A file-scope int x; is a tentative definition — it produces a GLOBAL, defined symbol x (a "common" symbol) in each file. So a.o provides a global x and b.o provides another global x. The linker's rule is: each needed symbol must match exactly one definition. Two definitions of the same global name → multiple-definition error (or, under older/looser rules, they silently coalesce into one shared variable — a nasty hidden aliasing bug). Fixes:

  1. If each file wants its own private x: write static int x; → makes it a LOCAL symbol (lowercase d/b), invisible to other files, no clash.
  2. If both should share one x: define it in one file (int x;) and write extern int x; in the other → the second becomes a U reference resolved to the single definition.

Level 4 — Synthesis

L4.1 — Design the relocation yourself

You are writing an assembler. Your code has lea rax, [rip + msg] — load the address of msg (in .rodata) using RIP-relative addressing, which is PC-relative. The instruction is 7 bytes; the 4-byte displacement is the last 4 bytes. Given the slot at , the next instruction at , symbol msg at : what addend must you emit, and what is the reloc type/formula?

Recall Solution

RIP-relative addressing computes [rip + disp] where rip at execution time equals the address of the next instruction, . We want that to equal : Matching the general PC-relative form , we read off . So: reloc type R_X86_64_PC32, addend , formula . The is once again the width of the displacement field sitting between and . See PC-relative addressing in x86-64.

L4.2 — C++ name mangling meets the symbol table

In C++, int add(int,int) and double add(double,double) can coexist. But an object file's symbol table has one name per symbol. How is the conflict avoided, and what will nm show?

Recall Solution

The compiler mangles each name to encode its parameter types, so the symbol names differ even though the source names match. Typical Itanium ABI mangling:

  • int add(int,int)_Z3addii
  • double add(double,double)_Z3adddd

These are two distinct symbols in the table, so there is no clash — overloading is implemented entirely at the symbol-name level. nm shows the mangled names (nm -C demangles them back to readable form). This is also why a C++ symbol won't link against a C caller unless you wrap it in extern "C" (which turns mangling off). See Name mangling in C++.


Level 5 — Mastery

L5.1 — Full end-to-end patch with a nonzero addend

A relocation references arr at symbol offset +16 (i.e. we want the address of arr[4] where each element is 4 bytes). The reloc is absolute R_X86_64_64 with addend . The linker places arr at . What address is written into the 8-byte slot?

Recall Solution

The addend lets a relocation point into the middle of a symbol. Absolute formula: , so — the address of arr[4]. The addend is how "address of arr plus offset 16" is expressed without a separate symbol.

L5.2 — Mixed program, count everything

File prog.c:

static int counter;      // (1)
int total = 100;         // (2)
extern int helper(int);  // (3)
int run(int n){          // (4)
    return helper(n) + total + counter;  // calls helper (5)
}

For prog.o, list every symbol with its nm letter and defined/undefined status, and count how many relocations the body of run needs (assume RIP-relative access to each global and a PC-relative call).

Recall Solution

Symbols:

  • counterstatic, uninitialised → b (local, .bss), defined.
  • total — global, initialised to 100 → D (global, .data), defined.
  • helper — declared extern, only calledU, undefined.
  • run — global function, has a body → T (global, .text), defined.

Relocations inside run: three memory references need patching:

  1. call helper → one PC-relative reloc (R_X86_64_PLT32, addend ) for symbol helper.
  2. RIP-relative load of total → one PC-relative reloc (R_X86_64_PC32, addend ) for total.
  3. RIP-relative load of counter → one PC-relative reloc for counter.

Total = 3 relocations. Note counter, though local, still needs a relocation because its final address isn't known until .bss is placed — defined does not mean address-known-at-compile-time.


Recall One-line self-test

The single rule that solves half these problems ::: S A P — write for absolute, for PC-relative; the turns an address into a distance.