The World of Linkers/ Labs/ 16 articles
3 min readPublic

[The World of Linkers—Lab 05] Calls Across Files: Merge the Code, Then Fill In the Displacements

Link several code-only objects into one executable so a call in one file reaches a definition in another. Theory 04 explains relocation formulas, the field address P, and the usual -4 addend on a call.

Before implementing numeric checks, read bit representations and extension checks in the same chapter. It explains modular 64-bit arithmetic, the difference between 32 and 32S, and why a large unsigned address can still fit a signed field's encoding.

Starting point: class5 code and tests, in the private repository; access is required. Reuse classes0–4 and implement three interfaces in the class5 crate library. Input objects remain immutable. The CLI is supplied.

Implementation tasks

TaskInterfaceRequired result
C5.1mergeMerge code in input order with alignment, recording each (file, section) contribution
C5.2applyValidate and write one relocation field; every failure leaves the destination unchanged
C5.3linkCompose parsing, global selection, layout, address translation, relocation, and ELF emission

class5 README defines the full contract. merge retains empty contribution identities and treats zero alignment as one. Duplicate identities are invalid; the final code must be nonempty. A contribution offset is relative to the merged code, before adding output headers or the virtual base.

apply supports NONE, 64, PC32, PLT32, 32, and 32S. PLT32 uses static direct binding here. offset indexes the destination slice; P is the runtime address of the field; S is the selected symbol's runtime value. P is not the next instruction address. NONE does not access a field. Other kinds check type, field bounds, and numeric range in that order, then write bytes.

Each input requires one symbol table. Accept PROGBITS/flags=6 code contributions; reject other nonempty allocated sections and COMMON. RELA must reference its object's symbol table and an emitted code contribution. Nonempty REL and debug-section relocations are rejected. Bound each patch to its own contribution slice, not the entire merged buffer. General symbol references may name an end label; the entry must remain strictly inside code.

Make one PC32 write concrete: let the relocation field have runtime address P = 0x4000b1, the target symbol runtime value be S = 0x4000c0, and the explicit addend be A = -4. The value written is S + A - P = 0x0b. The next instruction is at P + 4 = 0x4000b5, so execution reaches 0x4000b5 + 0x0b = 0x4000c0. Using the next-instruction address as P subtracts four twice, producing the most deceptive form of this bug.

Acceptance and what it establishes

From the cloned repository root, on native x86-64 Linux:

cargo test --locked -p class5
cargo test --locked -p class5 --release
python3 scripts/grade.py class5

Contract tests cover alignment, identity, negative addends, 32-bit extension checks, modular 64-bit arithmetic, and unchanged bytes on failure. Native tests assemble an entry point, compile a separate C function, and link and run them in both input orders. They also cover absolute calls, unresolved WEAK, missing definitions, forbidden data, and the CLI.

Successful cross-file execution checks that symbol selection and address translation work together. A separate contribution-boundary test prevents accepting a malformed field merely because it fits somewhere in the output buffer. Input order may change layout; acceptance requires correct behavior, not identical output bytes.