14.2 Symbols and Relocation
Separate compilation allows one translation unit to call a function or access data defined elsewhere. The compiler cannot know the final address of that entity, so it emits two coordinated records: a symbol describing the name and a relocation describing the machine-code or data field that must be repaired when the address becomes known.
A symbol-table entry commonly contains a name, binding, type, defining section, value, and size. In a relocatable file, the value is usually an offset within its input section rather than a final virtual address. A defined local symbol is visible only inside one object. A global symbol participates in cross-object resolution. An undefined symbol expresses a demand. ELF also supports weak bindings, section symbols, file symbols, and several visibility classes.
Resolving definitions by name and binding
Imagine three objects:
main.o: defines main (strong), needs render
fast.o: defines render (strong)
fallback.o: defines render (weak)The strong definition from fast.o wins, and the weak fallback is ignored. If both fast.o and another object provide strong render, the linker normally reports a multiple-definition error. If only a weak definition exists, it can satisfy the demand. If none exists, an executable link fails with an undefined-reference diagnostic unless the platform deliberately permits unresolved dynamic imports.
Traditional “common” symbols arose from tentative C definitions. Linkers could merge several commons, often selecting the largest alignment and size, but modern toolchains frequently default to ordinary definitions because accidental commons can conceal source errors. Local symbols do not collide across files. Symbol versioning, visibility, COMDAT groups, and language-specific name mangling add further policy; the core invariant remains that each reference must resolve to a definition with compatible semantics.
Relocation is deferred arithmetic
Consider an x86-64 call instruction whose displacement field begins at address P. The assembler knows the target symbol S by name but not by final address, so it writes placeholder bytes and emits a relocation such as R_X86_64_PC32 with addend A. The linker lays out sections, resolves S, then computes:
PC-relative relocation: value = S + A - P
absolute relocation: value = S + A
GOT-relative form: value = GOT(S) + A - PThe exact meaning of P, scaling, sign extension, and addend storage belongs to the ABI's relocation definition. Some formats keep the addend explicitly in the relocation entry (ELF RELA); others obtain it from the field being patched (ELF REL). An emitter must not guess. It selects a relocation kind that matches the instruction encoding and records the patch offset precisely.
Range, position independence, and relaxation
After computing a relocation value, the linker must prove it fits the encoded field. An 8-bit signed PC-relative field accepts only -128 through 127. A target 400 bytes away cannot be truncated; doing so silently redirects control flow. Depending on the architecture and linker's capabilities, an out-of-range branch causes an error, is relaxed into a longer instruction, or reaches a linker-generated thunk (veneer) that can jump farther.
Position-independent code avoids embedding load-address-dependent absolute addresses in read-only instructions. It favors PC-relative references for nearby code and data, and accesses preemptible external objects through indirection tables such as the Global Offset Table (GOT). The dynamic loader can then relocate writable table entries while keeping code pages shared and read-only. These choices begin in code generation: the relocation kind, instruction form, symbol visibility, and code model must agree.
Relocations also make dead-section elimination subtle. If an input section contains a relocation to another section, that edge can keep the target alive. COMDAT selection and identical-code folding must redirect symbol and relocation relationships consistently. Robust tests should inspect emitted relocation records, link at deliberately different base addresses, exercise positive and negative range boundaries, reject overflows, and round-trip known bytes through a trusted assembler or object-file reader. A relocation is small, but one wrong offset or formula can turn a correct compiler into a program-corrupting one.