Object files — .o - .obj, symbol table, relocation entries
WHAT is an object file?
WHY does it exist? So we can compile each source file separately (separate compilation) and only re-link when needed. Change one .c, recompile one .o, relink. Fast.
The pipeline (WHERE the .o sits)
file.c --[preprocessor]--> file.i --[compiler]--> file.s --[assembler]--> file.o --[linker]--> a.out

The Symbol Table
HOW to inspect it:
$ nm file.o # symbol table
$ readelf -s file.o # detailed ELF symbols
$ readelf -r file.o # relocation entries
$ objdump -d file.o # disassembly (shows placeholder addrs)
In nm output, a capital letter = global, lowercase = local:
T/t = text(code), D/d = data, B/b = bss, U = undefined.
Relocation Entries — the heart of it
Deriving the relocation formula from first principles
WHY a formula at all? Because the value to write depends on whether the reference is absolute (write the actual address) or PC-relative (write a distance from the patched instruction).
Let:
- = final address chosen for the symbol,
- = the addend (a constant stored in the reloc, e.g.
-4), - = the address of the place being patched (the slot itself).
HOW we got : A PC-relative branch on x86-64 computes its destination as
where is the byte just after the displacement field. We want (plus any addend). The addend is precisely set so that . Solving for the displacement we must store:
That is why the assembler emits addend = -4 for a 4-byte field: the 4 bytes between the patch slot and the end of the instruction.
Worked Example 1 — a global variable reference (absolute)
Source:
int g; // defined here (goes to .bss)
int *p = &g; // p is in .data, needs address of gThe bytes of p in .data must contain &g. But g's final address is unknown at compile time.
| step | what happens | Why this step? |
|---|---|---|
| 1 | g → symbol, DEFINED, section .bss |
this file provides g |
| 2 | p → symbol, DEFINED, section .data |
this file provides p |
| 3 | reloc added: offset of p, type R_X86_64_64, symbol g, addend 0 |
the 8 bytes of p are a placeholder for g's address |
| 4 | linker: chooses = address of g |
now address known |
| 5 | writes into p's bytes |
absolute formula |
Worked Example 2 — calling an external function (PC-relative)
Source main.c:
extern int add(int,int);
int main(){ return add(2,3); }| step | what happens | Why this step? |
|---|---|---|
| 1 | add → symbol, UND (undefined) |
main.c references but does not define add |
| 2 | main → symbol DEFINED in .text |
this file provides main |
| 3 | assembler emits call <placeholder> with reloc: offset = byte after call opcode, type R_X86_64_PLT32, symbol add, addend -4 |
call site needs a 32-bit PC-relative displacement |
| 4 | linker combines with add.o, picks = address of add, = patch slot |
addresses now fixed |
| 5 | writes into the 4 displacement bytes | PC-relative formula |
Why this step (5)? The CPU adds the stored displacement to the address of the next instruction; the -4 addend accounts for the 4 displacement bytes between and that next instruction, so the jump lands exactly on add.
Worked Example 3 — .bss saves disk space
int big[1000000]; // uninitialised globalThis is 1 MB of zeros. Why doesn't the .o grow by 1 MB? Because big goes into .bss, which records only a size, not actual bytes. The OS zero-fills it at load time. Why this matters: initialised data (.data) costs file space; zero-init (.bss) costs none.
Common Mistakes
Recall Feynman: explain to a 12-year-old
Imagine each code file is a worker who builds a piece of a Lego castle, but they don't know where their piece will sit in the final castle. So each worker writes sticky notes: "I built the drawbridge" (defined symbol) and "I need someone's tower here" (undefined symbol), plus "the door on my piece must point to the tower — fix the arrow later" (relocation). The linker is the boss who lays all pieces in one place, reads every sticky note, and draws the real arrows. Only then is the castle a real, playable thing.
Flashcards
What are the three kinds of content in an object file?
Why is an object file not runnable?
In nm output, what does a U mean?
What four things does a relocation entry contain?
Absolute relocation formula?
PC-relative relocation formula and why the extra −P?
Why is the addend often −4 for a 32-bit PC-relative call?
Why does a huge uninitialised global not enlarge the .o file?
Difference between a header error and undefined reference?
Difference between .o and .obj?
.o is the Unix/ELF naming, .obj is the Windows/COFF naming.Connections
- Linker — symbol resolution and relocation
- ELF and COFF file formats
- Static vs Dynamic Linking
- Compilation Pipeline — preprocess, compile, assemble, link
- Sections — .text .data .bss .rodata
- PC-relative addressing in x86-64
- Name mangling in C++
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Socho aap ne ek .c file likhi aur compile ki — jo nikalta hai woh .o (ya Windows pe .obj) object file hai. Ye ek adhoori program hai: machine code to ban gaya, par final memory addresses abhi tak fix nahi hue, kyunki baaki files aur libraries abhi mile nahi hain. Isliye object file do cheezein extra leke chalti hai — ek symbol table (main kaun-kaun se function/variable deta hoon, aur kaun-kaun se maangta hoon) aur relocation entries (kaun se exact bytes ko baad mein theek karna hai).
Symbol table ko aise samjho jaise ek meetup mein har banda do badge pehne hai: "Main ye de sakta hoon" (DEFINED) aur "Mujhe ye chahiye" (UNDEFINED, nm mein U). Linker ka kaam hai har "chahiye" ko sahi "de sakta hoon" se match karna. Jab aapko undefined reference to X error aaye, matlab kisi ne X maanga par kisi .o/library ne diya nahi — ye linker error hai, header wala compiler error nahi.
Relocation ka asli maza formula mein hai. Jab compiler call add likhta hai, use add ka address pata nahi, to woh ek placeholder (zero) daal deta hai aur note banata hai: "is offset pe add ka address bharna, type PLT32, addend -4". Linker jab address fix karta hai to do formula use karta hai: absolute ke liye S + A, aur PC-relative (jaise call/jmp) ke liye S + A − P. -4 isliye kyunki call instruction ke 4 displacement bytes ko adjust karna padta hai taaki jump bilkul sahi jagah land kare. Ek aur mast baat: bade uninitialised globals .bss mein jaate hain jahan sirf size store hota hai, bytes nahi — isliye file size nahi badhta. Yaad rakho: S A P se code patch hota hai!