A compiler emits each module not knowing where the others will sit in memory or what address an external function will have, so it leaves a relocation: “patch this slot with the address of f once you know it.” The linker lays the modules out at chosen base addresses, builds a global symbol table of every definition, then walks each relocation and writes base + offset into the slot. Separate compilation, resolved.
Module B calls f, which module A defines. The linker places A at one base and B at another, resolves f to baseA + offset, and patches B’s call slot. Watch the undefined symbol resolve and the relocation apply: live demo
“The linker just concatenates object files.” — no. It lays out sections at addresses, builds a cross-file symbol table, resolves every undefined reference to a definition (erroring on missing or duplicate symbols), and rewrites addresses via relocations. Concatenation is the easy part; relocation is the job. cited
A relocation is a tiny instruction to the linker: at this offset, of this type, add the address of this symbol. Static linking applies them at build time; dynamic linking defers them to load time. relocating loaders / Levine 1999
Resolving the external symbol and applying the relocation run on the canonical compiler: