14.2 符号与重定位
独立编译允许一个 translation unit 调用其他文件定义的函数或访问其中的数据。编译器此时不知道实体的最终地址,因此会发出两类相互配合的记录:符号(symbol)描述名称,重定位(relocation)描述等地址确定后,需要修补哪一个机器码或数据字段。
符号表项通常包含名称、绑定方式、类型、所在节、值与大小。在可重定位文件中,值一般是输入节内的偏移量,而不是最终虚拟地址。已定义的局部符号只在单个目标文件内可见;全局符号参与跨文件解析;未定义符号表达一项需求。ELF 还支持弱绑定、节/文件符号与多种可见性类别。
按名称与 binding 解析定义
假设有三个目标文件:
main.o: 强定义 main,需要 render
fast.o: 强定义 render
fallback.o: 弱定义 renderfast.o 的强定义胜出,weak fallback 被忽略。若 fast.o 与另一个文件都提供 strong render,链接器通常报告 multiple-definition error。只有 weak definition 时,它也能满足需求。若完全没有定义,可执行链接会报告 undefined reference;除非平台有意允许尚未解析的动态 import。
传统的"公有"符号(common symbol)来自 C 的暂定定义(tentative definition)。链接器曾允许合并多个公有符号,通常选择最大的对齐与大小;现代工具链则常把普通定义设为默认,因为意外的公有符号会掩盖源码错误。局部符号在不同文件之间不会冲突。符号版本控制、可见性、COMDAT 分组与语言名称修饰(name mangling)会增加更多策略,但核心不变量不变:每个引用都必须解析到语义兼容的定义。
Relocation 是延后执行的算术
考虑一条 x86-64 call 指令,其位移字段位于地址 P。汇编器知道目标符号名 S,却不知道其最终地址,所以先写占位字节,再发出带加数 A 的 R_X86_64_PC32 等重定位记录。链接器布局各节、解析 S,随后计算:
PC-relative relocation: value = S + A - P
absolute relocation: value = S + A
GOT-relative form: value = GOT(S) + A - PP 的精确定义、缩放方式、符号扩展以及加数的保存位置,都由 ABI 的重定位规范决定。有些格式把加数明确存入重定位条目(ELF RELA);另一些从被修补字段中读取(ELF REL)。代码生成器不能靠猜测,而要选择与指令编码匹配的重定位类型,并准确记录修补偏移量。
范围、位置无关与松弛
算出重定位值后,链接器必须验证它能放入编码字段。8 位有符号 PC 相对字段只能表示 -128 到 127;400 字节之外的目标不能直接截断,否则控制流会被静默导向错误地址。依据架构与链接器能力,越界跳转会导致错误、被松弛(relax)成更长指令,或先跳到链接器生成的、能覆盖更远距离的跳转桩(thunk/veneer)。
位置无关代码(position-independent code,PIC)避免把依赖装载基址的绝对地址嵌入只读指令。它倾向于用 PC 相对方式访问邻近代码和数据,并通过全局偏移表(Global Offset Table,GOT)等间接表访问可能被抢占的外部对象。动态加载器只需修改可写表项,代码页就能保持共享与只读。这些选择从代码生成阶段就已开始:重定位类型、指令形式、符号可见性与代码模型必须相互一致。
重定位还会让无用节消除(dead-section elimination)更微妙。如果输入节含有指向另一个节的重定位,这条依赖边可能使目标节保持存活。COMDAT 选择与等价代码折叠(identical-code folding)也必须一致地重定向符号与重定位关系。可靠测试应检查生成的重定位记录;用有意不同的基地址链接;覆盖正负范围边界;拒绝溢出;并用可信汇编器或目标文件读取工具对已知字节做往返验证。重定位记录虽小,但一个错误偏移量或公式足以让正确的编译器变成程序破坏器。