5.3.2Build Systems & Toolchain

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

2,265 words10 min readdifficulty · medium5 backlinks

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
Figure — Object files — .o  -  .obj, symbol table, relocation entries

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:

  • SS = final address chosen for the symbol,
  • AA = the addend (a constant stored in the reloc, e.g. -4),
  • PP = the address of the place being patched (the slot itself).

HOW we got S+APS+A-P: A PC-relative branch on x86-64 computes its destination as dest=Pnext+disp,\text{dest} = P_{\text{next}} + \text{disp}, where PnextP_{\text{next}} is the byte just after the displacement field. We want dest=S\text{dest} = S (plus any addend). The addend is precisely set so that A=(distance from P to Pnext)A = -(\text{distance from }P\text{ to }P_{\text{next}}). Solving for the displacement we must store: disp=S+AP.\text{disp} = S + A - P. That is why the assembler emits addend = -4 for a 4-byte field: the 4 bytes between the patch slot PP 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 g

The 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 SS = address of g now address known
5 writes S+0S + 0 into p's bytes absolute formula S+AS+A

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 SS = address of add, PP = patch slot addresses now fixed
5 writes S+(4)PS + (-4) - P into the 4 displacement bytes PC-relative formula S+APS+A-P

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 PP and that next instruction, so the jump lands exactly on add.


Worked Example 3 — .bss saves disk space

int big[1000000];   // uninitialised global

This 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?
Compiled sections (.text/.data/.bss/.rodata), a symbol table, and relocation entries.
Why is an object file not runnable?
It still has placeholder addresses and undefined symbols that only the linker can resolve and patch.
In nm output, what does a U mean?
An undefined symbol — referenced here but defined in another object file or library.
What four things does a relocation entry contain?
Offset into a section, the symbol referenced, the relocation type, and an addend.
Absolute relocation formula?
patched value = S + A (symbol address plus addend).
PC-relative relocation formula and why the extra −P?
S + A − P; because the CPU adds the displacement to PC, we must store a distance, not an address.
Why is the addend often −4 for a 32-bit PC-relative call?
It accounts for the 4 displacement bytes between the patch slot P and the next instruction the CPU uses as its base.
Why does a huge uninitialised global not enlarge the .o file?
It lives in .bss, which stores only a size; the bytes are zero-filled at load time.
Difference between a header error and undefined reference?
Header (missing declaration) is a compiler error; undefined reference is a linker error meaning no object/library defines the symbol.
Difference between .o and .obj?
Same concept; .o is the Unix/ELF naming, .obj is the Windows/COFF naming.

Connections

Concept Map

assembler stage

contains

contains

contains

records

records

patches bytes for

not runnable, fed to

matches needs to provides

resolves via

produces

enables

Source file .c

Object file .o / .obj

Sections .text .data .bss .rodata

Symbol table

Relocation entries

Defined symbols - provides

Undefined symbols - needs

Linker

Executable a.out

Separate compilation

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!

Go deeper — visual, from zero

Test yourself — Build Systems & Toolchain

Connections