[The World of Linkers—Lab 09] Link Before the Load Address Is Known
Produce a self-relocating static PIE. Linux may place its main image at different bases; supplied startup code repairs data pointers before entering user code, without libc or a dynamic loader. Theory 04 explains displacements versus addresses; Theory 06 explains load bias, target categories, and startup responsibilities.
Starting point: class9 code and tests, in the private repository; access is required. Reuse class8::prepare for archive selection, symbols, and GC, and class6 for layout. startup.S, build support, and the CLI are supplied. Build and test on native x86-64 Linux with GNU as and objcopy.
Implementation tasks
| Task | Interface | Required outcome |
|---|---|---|
| C9.1 | patch | Apply a static fixup, or zero a load-time pointer field and return Relative |
| C9.2 | encode_relative | Emit RELA in destination-RVA order, rejecting overlapping or out-of-range writes |
| C9.3 | emit | Validate reservations and entry, fill the startup descriptor, and emit ET_DYN |
| C9.4 | link | Count runtime records from Prepared, reserve space, lay out, patch, and emit an RVA map |
The class9 README defines types, the supported matrix, and checks. Value distinguishes Absolute from ImageRva even when their numbers match. Fixup.offset indexes a contribution slice; place_rva is that field's image-relative address. Subtract BASE when converting class6 addresses to RVAs, never from ABS constants.
Load-time pointer writes must fit initialized Data, not read-only storage, BSS, or segment gaps. NONE is an unconditional no-op; other failures preserve the destination. patch leaves competing-error precedence unspecified.
BOOT is a supplied 160-byte startup block whose last 32 bytes contain four little-endian u64 values: BOOT RVA, RELA RVA, record count, and user-entry RVA. The README gives exact ranges and reservation requirements. Startup recovers the actual base, applies records, and jumps to the entry while preserving the initial rsp and clearing rdx. This project descriptor is not a general ELF ABI structure.
The invariant chain to preserve
Keep the implementation in four stages, with each stage changing only its own coordinate system: Prepared retains input identities; class6 layout still uses the fixed BASE; patch handles a static value or turns a load-bias-dependent field into a Relative record within one contribution slice; encode_relative emits deterministic RELA bytes; only emit writes BOOT, RELA, and the entry descriptor into ET_DYN. Treating an RVA, a contribution-local offset, and a file offset as one integer can pass a low-address fixture while failing under ASLR or across contribution boundaries.
The pointer, BSS/COMMON, and non-PIC cases cover the three branches of that chain: static arithmetic, load-time relative arithmetic, and a forbidden absolute reference. Read the atomic patch tests before the reservation and execution tests; that separates arithmetic errors from storage-placement and startup errors.
Trace each task to its tests
| Task | Key tests | Property established |
|---|---|---|
C9.1 patch | static_displacements_and_absolute_values, runtime_pointer_and_failure_atomicity | Static values are written directly; runtime pointers produce only Relative; a failure leaves no partial mutation. |
C9.2 encode_relative | relative_record_encoding, relative_record_rejections | Records are encoded by RVA, while overlap, range, and reservation errors are rejected. |
C9.3 emit | pie_headers_and_descriptor, emission_rejects_bad_destinations | The result is an interpreter-free ET_DYN; the startup descriptor matches its reserved area and destinations are writable Data. |
C9.4 link | runtime_addresses_vary_with_aslr, archive_calls_and_initial_stack | One image starts at different bases while preserving archive selection, initial-stack, and entry conventions. |
Read the contract tests for local invariants, then the native tests for those invariants under execution; one suite cannot replace the other.
Acceptance and what it establishes
From the cloned repository root:
cargo test --locked -p class9cargo test --locked -p class9 --releasepython3 scripts/grade.py class9Contract tests cover target categories, atomic patches, record ordering and overlap, descriptors, headers, and destination validity. Native checks cover code/data pointers, BSS/COMMON, ABS/WEAK, negative addends, archives and GC, the initial stack, and rejection of non-PIC references and read-only runtime patches.
The ASLR fixture runs 12 times and reports its entry address; at least two distinct addresses must appear, not a distinct address on every run. ASLR must be enabled. Structural checks require ET_DYN without PT_INTERP/PT_DYNAMIC and preserved segment permissions. Passing establishes actual execution of the fixtures at varied bases, not support for arbitrary dynamically linked programs or shared libraries.
On macOS or a non-x86-64 Linux host, build.rs deliberately stops with class9 requires native x86-64 Linux. That message is the environment contract for the supplied startup assembly and native execution tests, not a failure in patch, emit, or the linker. Run these commands on a native x86-64 Linux machine (SSH is fine); a cross-compiled artifact cannot substitute for this acceptance.