链接器的世界/ 实验篇/ 16 篇
8 分钟阅读公开阅读

[链接器的世界-实验篇11] 看穿间接层:GOT、指令松弛与静态 TLS

本实验扩展静态 PIE,支持 GOT 引用、合同规定的指令松弛,以及 local-exec、initial-exec TLS。GOT 与松弛见原理篇 07,TLS 模板、线程指针和访问模型见原理篇 09。

起点:class11 代码与测试,位于 private 仓库,需要访问权限。在 class11 crate 的库接口中完成五组接口。复用 class8 的存活计划、class6 的布局、class9 的 PIE 补丁,以及 class10 的合并与展开记录处理;节分类、TLS 启动汇编、构建脚本和 CLI 已提供。

实现任务

任务接口要达到的结果
C11.1tls_layout、Tls::tpoff布局初始化与零填充模板,计算 TP-relative 偏移
C11.2lower按重定位种类、目标类别和指令字节选择处理方式,不改写输入
C11.3Got::offset、Got::encode按首次使用分配共享槽位,编码常量或请求加载期地址
C11.4emit校验 TLS 保留贡献,填写启动描述符并生成 PT_TLS
C11.5lower_with在同一次 PIE 布局中组合存活存储、合并常量、GOT、TLS 和展开信息,返回尚未序列化的结果

完整矩阵、合成身份、字段范围和诊断顺序见 class11 README。本章支持 LE/IE;GD、LD、TLSDESC 被明确拒绝。GOT 松弛只接受手册规定的 mov 字节条件,普通 GOTPCREL 与 ABS 目标保留槽位。

Prepared 表示输入身份、选中定义和存活集合;Lowered 增加布局、补丁和加载期记录。实现 lower_with 时保留分片映射并重建存活的展开记录,交给提供的 Lowered::emit 序列化。lower_prepared、lower_with_unwind 是调用这个任务的包装器。别名和排序参数用于后续 ICF;本章验证并使用它们,不负责证明代码等价。

启动顺序是 PIE 地址修正、TLS 模板复制、线程指针安装、用户入口。模板中的普通指针必须先成为运行时地址,再复制给线程;TLS 变量自己的定位则使用 TP 偏移。README 的坐标算例分别解释了这两类值和合并常量引用,避免重复应用加数。

实现时区分三种地址

在 x86-64 中,FS.base 是地址计算的基址,%fs:0 是从这个地址读取一个字;只有当启动代码把线程控制块的首字设为自身地址时,读取结果才等于线程指针(TP)。因此,FS.base、%fs:0 读取的内存内容和 TP + offset 是三个不同概念。输入符号也不能按“都在镜像内”归为一种地址:Image 符号随装载基址变化,Absolute 符号不变,TLS 符号使用模板内偏移,未定义 Weak 在找不到定义时取零。lower 必须先按这四种身份分类,再选择 GOT、TLS、松弛或普通修补路径。

GOT 槽保存的是“链接后要得到的值”,TLS 模板保存的是“每个线程复制的初始字节”,而 TP-relative 偏移是线程建立后才有意义的坐标。布局阶段先确定槽位、模板和保留区域;lower 阶段只决定指令采用 GOT、TLS 或松弛路径;启动阶段按“PIE 自重定位 → 复制 TLS 模板 → 安装 TP → 进入用户入口”执行。把模板内的普通指针当成 TP 偏移,或在复制模板后再次加装载基址,都会得到表面合法但运行时错误的地址。

基础样本必须与 class9 完全相同,说明新增功能没有改变无关路径;随后分别加入 GOT、TLS 和展开记录,才能把组合错误定位到对应坐标转换。

例如,一个 TLS 输入若有 .tdata 的 7 个初始化字节、.tbss 的 13 个零字节,最大对齐为 16,则模板的 image.len() 是 7、memory_size 是 20;为满足对齐,线程块可从模板偏移 32 开始放置线程指针。因此模板第 0 个字节的 TP-relative 偏移是 -32,.tdata 偏移 3 的字节的偏移是 -29,而最后 13 个字节只出现在每个线程的内存副本中,不出现在文件中。PT_TLS.p_filesz 应为 7,p_memsz 应为 20;把 p_memsz 当成文件长度会让启动复制越过输入图像。

GOT 也有同样的阶段边界:两个相同的 Slot 在第一次使用时只分配一个 8 字节槽,槽的值先以 Image、Absolute 或 Tls 身份保存,等布局确定后才编码。Image 槽的文件字节可以暂时为零,并生成一条加载期 Relative 记录;把这个零值当成最终地址,或在启动时再次把 TLS 偏移加上装载基址,都会得到错误结果。

从任务到测试

任务关键测试测试所证明的性质
C11.1 tls_layout/tpofftls_layout_orders_initialized_before_zero_fill、tls_metadata_precedes_size_calculation初始化字节先于零填充,模板文件大小与线程内存大小分离。
C11.2 lowerlowering_selects_got_relaxation_and_thread_offsets、lowering_rejects_tls_mismatches_and_models指令形式、TLS 模型和目标身份共同决定处理路径,不支持的组合被拒绝。
C11.3 Gotgot_deduplicates_in_first_use_order、got_encodes_constants_and_relative_records相同槽只分配一次;槽值与加载期 Relative 记录的生成时机正确。
C11.4 emittls_startup_descriptor_and_program_header、emit_rejects_bad_tls_reservationsPT_TLS、启动描述符和保留区域描述同一模板。
C11.5 lower_withthreads_receive_independent_blocks_from_pt_tls、lowering_preserves_layout_before_startup_serialization多线程复制彼此独立,组合布局在序列化前不被破坏。

先验证坐标和阶段边界,再验证启动代码;运行结果正确不能单独证明 PT_TLS 或 GOT 编码正确。

验收及其含义

在克隆后的仓库根目录、原生 x86-64 Linux 上执行;需要 GNU as 和 objcopy:

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

合同测试检查模板布局、处理方式、GOT 去重与记录、启动描述符和 PT_TLS。原生测试检查初始化/零填充 TLS、模板内指针、GOT 松弛与保留槽位,以及不支持的引用。

组合测试核对合并常量、GOT、TLS 和展开索引共用的坐标,并在 ASLR 下执行;第二线程测试只从 PT_TLS 构造副本,确认两个线程的值独立。前者验证后端组合,后者独立验证模板描述。没有 GOT、TLS、合并或展开输入的基础样本要求产物与 class9 完全相同。通过这些测试不表示实现了 libc 的完整线程运行库。