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

[链接器的世界-实验篇12] 让调试信息跟着地址走:DWARF、符号表与节头

本实验为 class10 的静态 ET_EXEC 与 class11 的静态 PIE 添加调试节、符号表和节头表。目标是在保留程序运行布局的同时,让 GDB、addr2line 和 readelf 能正确解释源码位置、符号与调试引用。DWARF 的地址坐标、tombstone 和调试文件组织见原理篇 11。

配套项目:class12 代码与测试,需要 private 仓库访问权限。保留上游完整的布局结果,沿用合并常量的分片映射和 TLS 模板。提供的 link 与 link_pie 分别连接 ET_EXEC 和静态 PIE 流程,CLI 通过 --pie 选择后者。

实现任务

在 class12 crate 的库接口中完成五项任务。输入类型、字段编码、错误规则与组合要求见 class12 README。

任务接口结果
C12.1collect_debug按名称收集 .debug_*,保留输入顺序和各输入的组内偏移
C12.2relocate_debug按目标坐标修补一个调试字段,失败时保持原字节
C12.3encode_symbols编码局部符号在前的 .symtab、共享名字的 .strtab 与 first_global
C12.4attach_sections追加非分配数据与节头表,更新 ELF 头中的节表字段
C12.5finish解析调试引用,生成输出节和符号,接入上游链接结果

本章有两项必须保留的约束。第一,调试引用具有不同坐标:普通图像值在 ET_EXEC 中是 VA,在 PIE 中是 RVA;调试节引用是节内偏移;DTPOFF 和 STT_TLS 使用 TLS 模块内偏移。ABS 常量与未定义 WEAK 的零值不参与图像基址换算,TLS 模块内偏移也不能替换成代码使用的 TP-relative 偏移。

第二,追加元数据不得改变入口、程序头或已分配内容。原文件前缀仅允许更新 e_shoff、e_shentsize、e_shnum 和 e_shstrndx;新增非分配内容不扩大 PT_LOAD。保留的符号沿用最终布局,GC 删除的代码引用写入 tombstone。具体取值、合并分片与 TLS 节身份的处理均由 README 定义。

可以用一组数值检查自己是否混用了坐标:假设 ET_EXEC 中某个符号的地址是 0x401120,同一布局用于静态 PIE 时其 RVA 是 0x1120;某个调试节在文件中的起点可能是 0x2a0,这不是代码地址;一个 TLS 符号的模块内偏移可能是 0x38,也不是线程指针相对偏移。调试重定位写入 .debug_* 时使用节内文件坐标,普通图像引用在 ET_EXEC 使用 VA、在 PIE 使用 RVA,STT_TLS 则使用模板内偏移。只有在启动阶段建立线程指针后,才把它转换成 TP-relative 地址。

支持范围为未压缩调试节、普通节编号和 README 列出的重定位;调试字符串直接拼接。压缩节、PC-relative 调试重定位、指向其他 metadata 的调试引用以及需要扩展节编号的输出,均返回相应错误。

实现时分成“保留图像”和“追加目录”

finish 不能重新布局上游结果。第一阶段只借用 Lowered 的最终地址和分片映射,解析调试重定位、符号值及 tombstone;第二阶段用这些稳定结果生成 .debug_*、.symtab、.strtab 和节头表,并只修改 ELF 头中允许变化的四个字段。这样可以分别验证“程序仍按原来的 PT_LOAD 运行”和“工具能按新目录解释它”。

GDB、addr2line、readelf 的检查分别覆盖源码行/回溯、符号地址和节头/重定位结构。若只看到程序返回 42,无法证明调试引用坐标正确;若只看到 readelf 能解析,也无法证明追加数据没有改变加载映射。

从任务到测试

任务关键测试测试所证明的性质
C12.1 collect_debugmultiple_compilation_units_keep_independent_debug_coordinates不同编译单元的节内坐标保持独立,不能把一个输入的偏移当成另一个输入的地址。
C12.2 relocate_debugsymbols_and_line_tables_agree_with_gnu_ld、rejects_unsupported_debug_inputs调试字段使用正确坐标,未支持的编码明确拒绝。
C12.3 encode_symbolssymbols_and_line_tables_agree_with_gnu_ld、zero_sized_tls_symbol_keeps_its_nobits_section_identity局部/全局顺序、字符串索引和 TLS/NOBITS 身份一致。
C12.4 attach_sectionsattach_appends_data_and_section_table、attach_respects_extended_numbering_boundary追加目录不改变已加载前缀,节头表字段和扩展编号边界正确。
C12.5 finishappending_sections_preserves_the_class8_image、cli_output_is_debuggable程序仍按原布局运行,同时工具能读取符号、行表和节头。

GDB、addr2line 和 readelf 的结果分别对应不同责任;任何一个工具能读都不能替代其余证据。

验收

在仓库根目录、原生 x86-64 Linux 上执行:

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

验收分别检查三个方面:

  • 接口合同:分组、对齐、坐标换算、失败原子性、符号顺序和节头编码。
  • 工具可读性:多编译单元的断点、源码行和跨文件回溯,对照 GNU ld 的 GDB 会话;用 addr2line 与 readelf 核对存活地址和 tombstone。
  • 组合完整性:对照上游产物检查原文件前缀,组合合并常量、CFI、GOT 与 TLS,并检查 CLI 和库输出一致。

测试中的 GDB 变量检查限于启动代码能够支持的范围。TLS 变量的自动定位还需要运行时发现协议;配套启动代码未提供完整的 libc/thread_db 协议,该能力不属于验收范围。