在计算机软件开发过程中,汇编器作为连接高级语言和机器代码的重要环节,承担着将汇编语言指令翻译成目标代码的职责。重定位生成作为汇编器的核心功能之一,对于实现可重定位目标文件并支持后续链接和加载阶段至关重要。理解重定位的生成机理,有助于深入掌握整个编译链接流程的底层细节,同时提升编译器、链接器和调试工具的开发设计水平。 重定位的概念本质上是为了处理在汇编阶段无法确定的地址信息。程序中的符号引用往往指向代码或数据的某个具体位置,这些位置在汇编完成时并未最终确定,而只有在链接或加载阶段才会被分配固定的内存地址。因此,汇编器通过生成重定位条目,标记那些依赖于符号地址但尚未确定的指令或数据字段,在链接期间由链接器填充具体值,实现内容修正。
典型的重定位对象是包括符号引用的指令操作数。例如在x86-64汇编指令movl sym(%rip), %eax中,汇编器需要计算程序计数器(PC)与符号sym的偏移距离来生成相对寻址的位移。然而,如果符号sym在汇编时未定义或者是外部符号,这个偏移就无法被立刻确定。这里汇编器会自动产生一个类型为R_X86_64_PC32的重定位条目,表示这一偏移需要链接器后续填充。 在GNU汇编器(gas)和LLVM集成汇编器(LLVM integrated assembler)中,生成重定位的过程依赖于多次传递与不同阶段的协同工作。在解析阶段,汇编器将汇编代码分割为多个代码段碎片,每个碎片中包含指令、数据和相关指令等。
每条指令的解析涉及操作码识别和操作数表达式分析,操作数中可能包含立即数、寄存器、或符号引用等。对符号或表达式的引用如果无法在此阶段完全折叠为确定的常量,则会被标记为“修正”(fixup)。 修正实际上绑定到代码碎片内的某个偏移位置,其值本质上是一个表达式,且必须最终能被评估为定位表达式。修正延后处理保证了后续链接符号或代码段地址的灵活性。 解析过程中汇编器还维护符号表,记录每个符号的定义位置与绑定类型,如本地符号(STB_LOCAL)、全局符号(STB_GLOBAL)、弱符号(STB_WEAK)等。这些信息对重定位的生成与处理有直接影响,尤其是符号是否需要外部链接或重定位引用。
随后进入代码布局阶段,汇编器根据代码大小、对齐要求对各段和碎片进行布局和地址偏移计算。此阶段完成符号在段内的具体偏移确定,而未定义或外部符号的地址依然交由链接器解决。这种分阶段设计使得汇编器能够先尽可能解析和确定一部分地址信息,提升代码执行效率及准确性。 紧接着,重定位判断阶段发挥关键作用。汇编器根据先前收集的修正项和符号信息,评估能否将修正表达式解析为常量。如果表达式完全可计算,汇编器会直接将数值嵌入指令编码,无需生成重定位条目。
但当表达式涉及未定义符号或须特殊处理的符号时,则会生成对应的重定位表项,指示链接器对该代码位置实施调整。 定位表达式常见表现为relocation_specifier(sym_a - sym_b + offset)形式,这里的relocation_specifier是重定位限定符,决定具体的重定位类型和计算方式。sym_a表示加数符号,sym_b为可选的减数符号,offset则是常量偏移。多数架构只支持单符号形式的表达,但如AVR和RISC-V允许使用符号差表达式,使得重定位机制更为灵活,也带来表达式校验的复杂性。 在PC相对重定位中,目标计算的值通常为符号地址减去当前指令地址,加上偏移量(S - P + A)。当符号是当前段内的本地非ifunc符号时,重定位可以完全解析为常量,但对于全局或弱符号,仍须生成重定位条目保证符号间的重定位机制得以执行。
这使得ELF符号的重定位与符号间的相互引用更加严谨。 重定位的解决结果分为三种类型:错误、解析成功、解析失败。错误是当表达式格式不合法或使用了不被支持的表达式类型时触发。解析成功意味着汇编器能直接将修正值编码进指令,无需后续调整。而解析失败则说明表达式需要链接器的帮助进行完成,汇编器生成对应的重定位字段,等待链接时符号地址的最终确定。 为进一步管理重定位的细节,汇编器引入“重定位限定符”这一概念,用于指示如何对符号地址进行处理和编码。
不同架构对重定位限定符的称谓和实现略有差异,如Arm架构称之为relocation specifier,GNU Binutils中通常叫做modifier,AVR则称为relocatable expression modifiers。现代汇编器趋向采用relocation specifier一词,既便于理解又适合汇编阶段的表达式调整。 常见的重定位限定符语法呈现多样形式,包括expr@specifier、%specifier(expr)、expr(specifier)以及:specifier:expr等。expr@specifier是许多binutils目标通用的后缀样式,便于表述符号与修饰符关系,常见于PowerPC、x86以及Mach-O等目标。%specifier(expr)广泛见于MIPS、RISC-V和SPARC,它通过括号清晰界定表达式作用范围,同时区分绝对和PC相对定位。expr(specifier)和:specifier:expr主要出现在AArch32和AArch64,分别使用括号和冒号框定,解决语法歧义问题。
这种多样化的限定符系统保证了汇编器和链接器能够精准识别各种目标平台的特殊需求。例如在RISC-V中,%lo和%pcrel_lo明确区分绝对地址和PC相对偏移,结合lui、auipc等指令一同实现高效的地址计算与加载。 另外,线程局部存储(TLS)符号因其特殊的存储类型与访问方式,在汇编器中也采用专门的重定位限定符进行标识,如AArch64的:tprel_hi12:和x86的@TPOFF。TLS重定位支持使得多线程应用可以正确访问线程专有数据。 复合重定位是另一个值得关注的主题。某些指令产生多个重定位条目,这些条目联合表示一个复杂的定位表达式,例如PowerPC的GOT间接寻址和RISC-V的链接器松弛机制。
复合重定位通过顺序应用多个重定位,实现复杂地址调整,链接器在处理时必须考虑条目间的组合和顺序关系。 内存映像的最终生成还涉及隐式加数的处理,尤其是在ELF SHT_REL和Mach-O中。这使得指令中部分偏移无需显式存储,减少目标文件尺寸。同时GNU汇编器和LLVM汇编器在内部实现上也有所不同,GNU采用包含加数和减数符号的fix结构统一表示修正与重定位,而LLVM则将修正和表达式分开管理,通过MCFixup和MCValue等类进行表达和追踪。 LLVM的设计倾向于面向表达树,对子表达式的重定位限定符处理存在一定的局限和改进空间,未来计划将重定位限定符直接编码进MCFixup,以简化表达式处理和提升效率。解析器和打印器同样对重定位表达式的支持有着不同的实现策略,确保汇编代码能够被准确解析和反汇编。
综上所述,汇编器中的重定位生成机制是现代编译工具链中不可或缺的一环,连接了汇编语言指令翻译与链接器符号地址解析的桥梁。通过多阶段处理符号和表达式,在解析、布局、判断和生成重定位条目中保持灵活和准确,保证了生成的目标文件能正确、高效地被链接器处理。对于涉及不同架构和目标文件格式的多样化需求,重定位机制通过限定符和表达式设计实现高度适配。深入理解重定位生成的工作原理,不仅能帮助编译器开发者优化工具链性能,还能促进逆向工程、二进制分析等领域的研究与实践,进而推动整个软件生态的革新与发展。