[链接器的世界-实验篇09] 不知道加载地址时怎么链接:自重定位的静态 PIE
本实验生成自重定位的静态 PIE。Linux 可以把主映像放到不同基址;提供的启动代码在用户入口之前修正数据指针,程序不依赖 libc 或动态加载器。位移与绝对地址的区别见原理篇 04,加载偏移、目标类别与启动责任见原理篇 06。
起点:class9 代码与测试,位于 private 仓库,需要访问权限。复用 class8::prepare 的归档选择、符号解析和 GC,以及 class6 的布局。startup.S、构建脚本和 CLI 已提供。构建与测试在原生 x86-64 Linux 上执行,需要 GNU as 和 objcopy。
实现任务
| 任务 | 接口 | 要达到的结果 |
|---|---|---|
| C9.1 | patch | 应用静态补丁,或将加载期指针字段清零并返回 Relative |
| C9.2 | encode_relative | 按目标 RVA 排序输出 RELA,拒绝重叠与越界写入范围 |
| C9.3 | emit | 校验保留区域与入口,填写启动描述符,输出 ET_DYN |
| C9.4 | link | 从 Prepared 统计运行时记录、预留空间、布局、修补并输出 RVA map |
完整类型、支持矩阵和检查规则见 class9 README。Value 区分 Absolute 与 ImageRva,即使数值相同也不能互换;Fixup.offset 是贡献切片内的位置,place_rva 是这个字段在映像中的 RVA。class6 的地址转成 RVA 时减去 BASE,ABS 常量不做这项转换。
本章只允许在已初始化 Data 中修正加载期指针;只读内容、BSS 和段间隙不是合法目的地。NONE 无条件不操作,其他失败必须保持原字节;同时违反多条规则时,patch 不规定错误优先级。
BOOT 是项目提供的 160 字节启动块,后 32 字节保存四个小端 u64:BOOT RVA、RELA RVA、记录条数和用户入口 RVA。具体范围与保留贡献的布局要求在 README 中。启动代码据此恢复实际基址、应用记录并跳到用户入口,保留初始 rsp、清零 rdx。这份自定义描述符不属于通用 ELF ABI。
实现时保持的一条不变量链
把实现拆成四个阶段,并让每个阶段只改变自己负责的坐标:Prepared 仍保存输入身份;class6 的布局结果用固定 BASE 表示;patch 在贡献切片内处理静态值或把可随装载基址变化的字段变成 Relative;encode_relative 只负责确定性的 RELA 字节;emit 最后才把 BOOT、RELA 和入口描述写入 ET_DYN。只要把 RVA、贡献内偏移和文件偏移混在一个整数里,低地址样例仍可能通过而 ASLR 或跨贡献边界会失败。
验收中的指针初始化、BSS/COMMON、非 PIC 拒绝分别覆盖这条链的三种分支:静态值、运行时相对值和不允许的绝对引用。先在 patch 的独立测试里验证原子性,再看 emit 的保留区域和真实执行;这样可以判断错误来自算术、存储位置还是启动代码。
从任务到测试
| 任务 | 关键测试 | 测试所证明的性质 |
|---|---|---|
C9.1 patch | static_displacements_and_absolute_values、runtime_pointer_and_failure_atomicity | 静态值直接写入;运行时指针只产生 Relative;失败不会留下半次修改。 |
C9.2 encode_relative | relative_record_encoding、relative_record_rejections | 记录按 RVA 编码,重叠、越界和非法预留都会被拒绝。 |
C9.3 emit | pie_headers_and_descriptor、emission_rejects_bad_destinations | 输出是无解释器的 ET_DYN,启动描述符与保留区域一致,写入目的地属于可写 Data。 |
C9.4 link | runtime_addresses_vary_with_aslr、archive_calls_and_initial_stack | 同一镜像可在不同基址启动,并保留归档选择、初始栈和入口约定。 |
先读合同测试确认局部不变量,再读原生测试确认这些不变量在真实执行中仍成立;只通过其中一组不能替代另一组。
验收及其含义
在克隆后的仓库根目录执行:
cargo test --locked -p class9cargo test --locked -p class9 --releasepython3 scripts/grade.py class9合同测试检查目标类别、补丁原子性、记录排序和重叠、描述符、程序头与合法目的地。原生测试检查代码和数据指针、BSS/COMMON、ABS/WEAK、负加数、归档与 GC、初始栈,以及非 PIC 引用和只读补丁的拒绝。
ASLR 用例执行 12 次,读取程序报告的入口地址,要求出现至少两个不同地址;并不要求每次都不同。该用例要求系统启用 ASLR。结构检查要求 ET_DYN、无 PT_INTERP/PT_DYNAMIC,并保持既定段权限。通过这些检查说明提供的程序在实际不同基址下可运行,不意味着支持任意动态链接程序或共享库。
如果在 macOS 或非 x86-64 Linux 上执行,build.rs 会在编译阶段主动返回 class9 requires native x86-64 Linux。这是启动汇编和原生执行验收的环境合同,不是 patch、emit 或链接逻辑的测试失败;应把仓库放到原生 x86-64 Linux(可通过 SSH 连接)后再运行本节命令,不能用交叉编译结果替代这项验收。