[链接器的世界-实验篇14] 接住 rustc:把链接器变成真正的驱动程序
本实验为累积的 class13 静态 PIE 后端添加命令行入口。完成后,rustc 可以直接调用你的链接器,传入目标文件、.rlib 归档和库搜索路径,生成能在原生 x86-64 Linux 上运行的 Rust 程序。
起点:class14 配套代码与测试。仓库保持 private,需要访问权限。后端、节分类策略、程序入口和 Rust 测试程序均已提供;本次补全驱动层,不调用系统 ld 代替链接工作。Rust 的编译与链接关系见原理篇 14。
实现任务
在 class14 crate 的库接口中补全四个接口:
| 任务 | 接口 | 完成后交给下一步的结果 |
|---|---|---|
| C14.1 | parse | 将参数解析为 Invocation,保留输入与搜索目录的顺序;不访问文件系统 |
| C14.2 | find_library | 按搜索目录顺序找到静态归档路径 |
| C14.3 | expand | 就地展开 response file,保留参数字节,限制递归深度 |
| C14.4 | run | 展开、解析、加载输入,调用提供的 class14::link,写出权限为 0755 的文件 |
完整选项表、接口类型和错误规则见 class14 README。特别注意:这里始终链接静态归档;接受 -Bdynamic 不会搜索共享库,接受 --eh-frame-hdr 也不表示输出展开索引。测试程序采用 panic=abort,驱动显式选择 AbortOnly;这只省略运行时展开记录,仍通过 class13 完成 ICF、class11 完成 PIE/GOT/TLS 与常量合并,再由 class12 生成符号表和调试元数据。支持范围不包含异常展开或运行时回溯。
Response file 的规则也以 README 为准:
例如参数 @args.rsp,而文件内容为 "lib dir/a.o" -lfoo,展开结果应是两个 OsString:lib dir/a.o 与 -lfoo;带空格的路径不能被拆成两个参数。展开阶段只产生有序参数,不读取 ELF,也不决定符号;随后 parse 形成 Invocation,run 才按搜索目录读取对象和归档。嵌套展开最多十六层,无法读取的 @name 按 README 的规则保留或报错,不能静默改成普通文件名。无法读取的 @PATH 保留原参数;未闭合引号延续到文件末尾;末尾单独的反斜线被丢弃。这些是本题明确接受的行为。Linux 路径不要求 UTF-8,因此接口使用 OsString/PathBuf,不能通过有损字符串转换处理路径。
用完成的驱动链接 Rust 程序
实现四个接口后,在仓库根目录执行以下命令。输入程序已提供于 class14/tests/fixtures/no_std_fmt.rs;所有输出由下面的命令创建。
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 指定本次调用的可执行文件,绝对路径避免它被当作系统 PATH 中的命令查找。linker-flavor=ld 让 rustc 使用直接传给链接器的参数形式;本驱动不是 cc 编译器驱动。panic=abort 与后端的 AbortOnly 策略一致;force-unwind-tables=no 不等于承诺依赖归档完全不含 CFI,驱动仍须按已定义策略处理这些输入。本机 rustc 会从自己的 sysroot 选择 core 与 compiler_builtins,不需要切换 musl 目标。
./target/reader-class14/no_std_fmtprintf 'exit=%s\n' "$?"readelf -hW target/reader-class14/no_std_fmtreadelf -lW target/reader-class14/no_std_fmt程序应输出 sum=42 [2, 3] 1.50,随后显示 exit=42。非零退出码在这里是测试程序主动报告求和结果,不是链接失败;若在启用 set -e 的脚本中执行,要显式捕获它。程序头检查还应确认 ET_DYN,且不包含 PT_INTERP/PT_DYNAMIC。输出正确与结构正确是两项检查,不能相互替代。
把驱动层和链接后端分开验证
parse、expand 和 find_library 都是纯接口:它们只产生有序的 Invocation 或路径,不应提前读取 ELF 或改变输入。run 才负责读取文件、区分对象与 archive、调用累积后端并写出 0755 文件。先用字节级合同测试固定参数语义,再用 rustc 调用链验证驱动没有丢失参数顺序、非 UTF-8 路径或 response-file 内容。
Rust 程序的 stdout/exit status 证明入口和运行时约定,ET_DYN 与程序头检查证明输出格式,GDB/DWARF 检查证明 class12 元数据仍然可用;这些证据分别对应不同责任,不能用其中一项替代全部验收。
从任务到测试
| 任务 | 关键测试 | 测试所证明的性质 |
|---|---|---|
C14.1 parse | parse_accepts_rustc_command_lines、parse_rejects_unsupported_options | 驱动保留 rustc 参数顺序,并明确拒绝超出支持矩阵的选项。 |
C14.2 find_library | libraries_are_searched_in_order | 搜索结果由目录顺序决定,不被宿主环境的偶然顺序替换。 |
C14.3 expand | response_files_follow_gnu_quoting、response_files_nest_up_to_sixteen_levels | response file 的字节、引号和递归边界可重现。 |
C14.4 run | rustc_links_a_no_std_program、driver_preserves_cumulative_pie_features | 驱动把真实 Rust 输入交给累积后端,并保留 PIE、TLS、GOT 和调试能力。 |
参数合同测试证明驱动语义,真实 rustc 测试证明驱动没有在进程边界丢失这些语义。
验收及其含义
在克隆后的仓库根目录、原生 x86-64 Linux 上执行:
cargo test --locked -p class14cargo test --locked -p class14 --releasepython3 scripts/grade.py class14合同测试检查参数解析、库搜索顺序、非 UTF-8 路径、response file 引号与嵌套边界。原生测试让 rustc 编译提供的 no_std_fmt.rs,使用你的驱动链接真实的 core 和 compiler_builtins 归档;程序应输出 sum=42 [2, 3] 1.50 并以 42 退出,多次运行结果一致。程序头检查确认产物为 ET_DYN,且没有 PT_INTERP/PT_DYNAMIC。另有用例检查库输入、缺失库、不支持的选项和未解析引用。
组合用例检查真实 GOT 槽、TLS、相同常量的共享存储、代码折叠、别名调试引用的 tombstone,以及驱动和库输出一致。另将真实 Rust 程序以 debuginfo=2 编译,要求 DWARF 解码无警告,GDB 能在 rust_entry 按源码定位并运行到退出;调试信息与运行时展开是两个独立能力。
通过意味着本课程指定的 rustc 调用链可用,不能据此推断支持全部 GNU ld 选项、任意 Rust 程序或动态链接。