Exercises — Object files — .o - .obj, symbol table, relocation entries
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. astaticfunction), defined here.D— in.data(an initialised global), defined here.B— in.bss(a zero-initialised global), defined here.U— undefined: this file uses the name but does not provide it. The linker must find it in another.oor 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 bytes07 00 00 00must be stored in the file.int b;→.bss. A global with no initialiser starts at zero;.bssrecords only a size, not bytes, so it costs no file space.- the string literal
"hi"→.rodata(read-only constants). The pointermsgitself 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
L3.1 — Diagnose the link error
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 =
mainbase + 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:
- If each file wants its own private
x: writestatic int x;→ makes it a LOCAL symbol (lowercased/b), invisible to other files, no clash. - If both should share one
x: define it in one file (int x;) and writeextern int x;in the other → the second becomes aUreference 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)→_Z3addiidouble 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:
counter—static, uninitialised →b(local,.bss), defined.total— global, initialised to 100 →D(global,.data), defined.helper— declaredextern, only called →U, undefined.run— global function, has a body →T(global,.text), defined.
Relocations inside run: three memory references need patching:
call helper→ one PC-relative reloc (R_X86_64_PLT32, addend ) for symbolhelper.- RIP-relative load of
total→ one PC-relative reloc (R_X86_64_PC32, addend ) fortotal. - RIP-relative load of
counter→ one PC-relative reloc forcounter.
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.