[The World of Linkers—Lab 14] Take the Linker Call from rustc
Give the cumulative class13 static-PIE backend a command-line entry point. By the end of this lab, rustc will invoke your linker with object files, .rlib archives, and library search paths, producing a Rust program that runs on native x86-64 Linux.
Starting point: class14 code and tests, in the private course repository; access is required. The backend, section policy, executable entry point, and Rust fixture are supplied. Implement the driver layer without delegating the link to the system ld. Theory 14 explains Rust's compilation and linking model.
Implementation tasks
Complete these four interfaces in the class14 crate library:
| Task | Interface | Result passed to the next stage |
|---|---|---|
| C14.1 | parse | An Invocation preserving input and search-directory order, without filesystem access |
| C14.2 | find_library | The first matching static archive in search-directory order |
| C14.3 | expand | Arguments expanded in place, preserving bytes and respecting the recursion limit |
| C14.4 | run | Expanded and parsed inputs loaded, linked through the supplied class14::link, and written with mode 0755 |
The class14 README defines the option table, API types, and error rules. This driver always links static archives: accepting -Bdynamic does not enable shared-library searches, and accepting --eh-frame-hdr does not emit an unwind index. The fixture uses panic=abort and the driver explicitly selects AbortOnly. This omits runtime unwind records while retaining class13 ICF, class11 PIE/GOT/TLS and constant merging, and class12 symbols and debug metadata. Exception unwinding and runtime backtraces remain outside the driver contract.
For example, if @args.rsp names a file containing "lib dir/a.o" -lfoo, expansion produces two OsString values: lib dir/a.o and -lfoo; the path with a space must not be split. Expansion only produces ordered arguments. parse then builds an Invocation, and only run reads objects and archives from the search paths. Nesting is limited to sixteen levels; unreadable @name follows the README rule and must not silently become an ordinary file name.
Response-file behavior is also explicit: an unreadable @PATH stays as an argument; an unterminated quote extends to EOF; a trailing backslash is dropped. These are accepted cases in this contract. Linux paths need not be UTF-8, so preserve OsString/PathBuf values rather than converting them to lossy strings.
Link a Rust program with the completed driver
After implementing the four interfaces, run these commands from the repository root. The input is supplied at class14/tests/fixtures/no_std_fmt.rs; the commands create their output files.
cargo build --locked -p class14 --bin class14course_linker="$PWD/target/debug/class14"mkdir -p target/reader-class14rustc --edition 2024 \ -C panic=abort -C opt-level=2 -C force-unwind-tables=no \ -C linker-flavor=ld -C linker="$course_linker" \ class14/tests/fixtures/no_std_fmt.rs \ -o target/reader-class14/no_std_fmtlinker names the executable to invoke; the absolute path avoids treating it as a command to find through PATH. linker-flavor=ld selects arguments passed directly to a linker: this driver is not a cc compiler driver. panic=abort matches the backend’s AbortOnly policy. force-unwind-tables=no does not promise that dependency archives contain no CFI; the driver must still apply its defined input policy. Native rustc selects core and compiler_builtins from its own sysroot, without switching to a musl target.
./target/reader-class14/no_std_fmtprintf 'exit=%s\n' "$?"readelf -hW target/reader-class14/no_std_fmtreadelf -lW target/reader-class14/no_std_fmtExpect sum=42 [2, 3] 1.50 followed by exit=42. This nonzero status is the fixture’s intentional sum result, not a link failure; a script using set -e must explicitly capture it. Also confirm ET_DYN and the absence of PT_INTERP/PT_DYNAMIC. Correct output and correct ELF structure are separate checks.
Verify the driver separately from the backend
parse, expand, and find_library are pure interfaces: they produce an ordered Invocation or path and must not read ELF inputs early. run owns file loading, object/archive classification, the cumulative backend call, and the 0755 output. Use byte-level contract tests to fix argument semantics first, then the rustc path to prove that ordering, non-UTF-8 paths, and response-file content survive the driver boundary.
The Rust program's stdout and exit status test entry and runtime conventions; ET_DYN and program-header checks test the output format; GDB/DWARF checks test class12 metadata. Each evidence layer covers a different responsibility.
Trace each task to its tests
| Task | Key tests | Property established |
|---|---|---|
C14.1 parse | parse_accepts_rustc_command_lines, parse_rejects_unsupported_options | The driver preserves rustc argument order and rejects options outside its support matrix. |
C14.2 find_library | libraries_are_searched_in_order | Search results follow directory order rather than incidental host ordering. |
C14.3 expand | response_files_follow_gnu_quoting, response_files_nest_up_to_sixteen_levels | Response-file bytes, quoting, and recursion limits are reproducible. |
C14.4 run | rustc_links_a_no_std_program, driver_preserves_cumulative_pie_features | The driver sends real Rust inputs through the cumulative backend while preserving PIE, TLS, GOT, and debug behavior. |
Argument contract tests establish driver semantics; the real rustc test establishes that the process boundary does not lose them.
Acceptance and what it establishes
From the cloned repository root, on native x86-64 Linux:
cargo test --locked -p class14cargo test --locked -p class14 --releasepython3 scripts/grade.py class14Contract tests cover parsing, ordered library searches, non-UTF-8 paths, quoting, and nested response files. Native tests compile the supplied no_std_fmt.rs through rustc and your driver, pulling in real core and compiler_builtins archives. The program must print sum=42 [2, 3] 1.50, exit with status 42, and behave consistently across repeated runs. Program-header checks require ET_DYN with neither PT_INTERP nor PT_DYNAMIC. Other cases exercise libraries, missing libraries, unsupported options, and unresolved references.
A combined fixture checks a real GOT slot, TLS, shared constant storage, code folding, alias debug tombstones, and driver/library equivalence. A real Rust build with debuginfo=2 must decode without DWARF warnings and support a GDB source breakpoint at rust_entry, followed by execution to the expected exit. Debug metadata and runtime unwinding are separate capabilities.
Passing establishes that the specified rustc invocation works. It does not establish support for all GNU ld options, arbitrary Rust programs, or dynamic linking.