news 2026/10/18 21:21:52

链接器重定位原理详解:从ELF目标文件到GOT/PLT动态解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
链接器重定位原理详解:从ELF目标文件到GOT/PLT动态解析

链接器的理解:重定位

很多年以前我第一次自己写一个超过两个源文件的项目时,遇到了一件让我困惑很久的事:我明明在一个文件里定义了一个全局函数,在另一个文件里也声明了extern,编译都通过了,为什么链接阶段却报undefined reference?后来我才明白,编译器的每个编译单元是独立工作的,它根本不知道别的编译单元里有什么,只有链接器把所有目标文件拼在一起的时候,才知道这个函数到底在哪。而“到底在哪”这件事,正是重定位要解决的核心问题。

重定位(relocation)是链接过程中最机械、最枯燥、但也是最关键的一步:把目标文件里那些“假设地址从0开始”的指令和数据,修正为它们在最终可执行文件或共享库里的真实地址。如果你写C/C++,理解重定位能帮你弄清楚为什么会出现undefined reference、为什么会链接报错、为什么静态库顺序有讲究、为什么动态库会有GOT和PLT这些东西。这篇内容就把重定位整个拆开,从目标文件的视角讲到链接器的具体操作,再翻译成人话讲清楚。

做嵌入式、做Linux开发、写驱动、或者碰到链接脚本的开发者,这篇文章会比较有参考价值;如果你只是应用层写业务,理解重定位也能让你在排查编译错误时少走很多弯路,至少看到那一大串链接报错不会两眼一抹黑。

我计划按这个顺序讲:

  1. 重定位根本上要解决什么问题,为什么编译完的代码不能直接跑
  2. 目标文件里有哪些数据结构在支撑重定位,长什么样
  3. 静态链接时重定位的完整计算过程,手算一遍
  4. 动态链接时又有什么不同,为什么这么设计
  5. 实际开发中常见的重定位错误、排查思路和避坑经验

1. 为什么要重定位:编译器的“绝对假设”与链接器的“统一编址”

先说一个最基础的认知:当你用gcc编译一个源文件时,得到的.o目标文件里的代码,并不是最终能直接执行的机器码。编译器在生成汇编和机器码时,并不知道这个文件会被放在内存的哪个位置,更不知道别的文件里的函数和变量地址是多少。

1.1 编译期的地址与链接期的地址是两个世界

编译器(更准确说是汇编器)在生成目标文件时,默认所有节(section)的起始地址都从0开始往下排。也就是说,如果某个编译单元里函数A在.text节偏移0x100处,函数B在0x200处,那么函数A调用函数B时,编译器会在机器码里填入一个相对偏移或者一个绝对地址,但这个地址是“占位”的,它的参考系是那个编译单元自己虚拟出的0地址,而不是最终程序加载到内存后的真实地址。

这些占位地址可以分为两类:

  • 不需要修正的:如果目标文件里某个指令只引用同一个节内的符号,且位置固定、偏移可算,那么通常可以用相对寻址直接算好偏移,链接时不需要额外修正。这也是为什么局部static函数、局部变量不会产生重定位项。
  • 需要修正的:一旦引用跨节(比如去引用.rodata里的字符串)、跨文件(去引用别的编译单元定义的函数或全局变量),编译器就没法知道目标地址了。它只会在指令里放个占位地址,同时往重定位表里记一条:“这条指令的此处,将来需要填入某个符号的真实地址”。

对现代x86-64架构来说,绝大多数函数调用和跳转都是相对寻址(rip相对寻址),所以编译期反而能算出很多偏移。但你去看那些直接引用全局变量的指令,比如mov某个全局变量的值到寄存器,往往就会产生重定位项。还有取函数指针、访问静态数据地址,也会产生。

1.2 打个比方:搬新家和换地址

你可以把每个编译单元想象成一间办公室:这间办公室里的人(指令)在开会,提到别间办公室同事(跨文件符号)的时候,只能先在白板上写个“小王在两小时后知道地址”,不能写“小王在某号楼某房间”,因为地址还没定。链接器做的事情就是把所有办公室搬到一栋大楼里(最终可执行文件或共享库的地址空间),然后挨个把白板上的“待定地址”换成真实房间号。

这个类比其实非常贴近重定位的本质:不是“修改代码逻辑”,而是“修正代码中数据的地址信息”。指令码本身不变,变的是指令里携带的操作数——那个内存地址。

1.3 重定位的必要性在什么场景下最明显

如果你是单文件编程,重定位的问题确实不明显,因为单个文件内部的相对调用和跳转用不到重定位,甚至gcc -fno-pie配合固定的链接脚本可以生成起始地址固定的可执行文件。但一旦涉及:

  • 多个源文件之间的函数互相调用
  • 引用外部全局变量
  • 使用第三方静态库
  • 编译位置无关代码(-fPIC)制作共享库
  • 自定义链接脚本摆放节的位置

重定位就是绕不开的话题。尤其当你自己写一个链接器脚本,想控制某个段的加载地址时,你会看得格外清楚:哪怕你把代码挪到了指定地址,那些引用外部符号的指令依然要靠重定位表才能填对地址,否则程序一跑就崩。

2. 目标文件里的重定位信息长什么样:ELF视角下的数据结构

要理解重定位,先看懂目标文件的格式。以Linux下的ELF为例,一个编译出来的.o文件里有几块关键内容:节头表(section header table)、符号表(symbol table)、重定位节(relocation sections)。这里我们不谈ELF的完整结构,只揪出与重定位直接相关的部分。

2.1 深入剖析ELF中的重定位节

重定位信息不是散落在代码里的,而是集中在专门的节中。以x86-64架构为例,每个需要重定位的节,都会配一个对应的重定位节:

  • .text节的重定位信息放在.rela.text里
  • .data节的重定位信息放在.rela.data里
  • .rodata节的重定位信息放在.rela.rodata里

注意这些重定位节的标志位里通常带着“INFO”属性(SHF_INFO_LINK),表示它们关联到某个节和符号表。我们可以用readelf命令把它们的内容打印出来看。

2.2 重定位项的数据结构和字段含义

ELF64标准里,重定位项(以rela类型为例)是这样一个结构:

Elf64_Rela的结构体,包含三个关键字段:r_offset、r_info和r_addend。其中r_offset表示需要修正的位置在目标节内的偏移量,r_info保存了符号表索引和重定位类型,r_addend是计算中要用到的修正量常量。

readelf -r会显示这些字段的解析结果,比如符号、类型、偏移量。下面是一个具体的例子说明:

我写一个简单的demo,文件名main.c:

int external_var = 42; extern void external_func(void);

void main_func(void) { external_func(); }

用gcc -c main.c编译后,再用readelf -r main.o看看:

Relocation section '.rela.text' at offset 0x... contains 1 entry: Offset Info Type Sym. Value Sym. Name + Addend 000000000015 000700000002 R_X86_64_PC32 0000000000000000 external_func + fffffffc

注意这里看到的信息非常清晰:需要修正的位置在.text节内偏移0x15处,重定位类型是R_X86_64_PC32(PC相对32位偏移),引用的符号是external_func,addend是-4。这个-4初看很奇怪,后面我们会算明白它为什么是-4。

2.3 重定位类型:每种类型就是一套不同的计算规则

x86-64下常见重定位类型有好几种,不同场景选不同规则。我列一个对比表,这些你在readelf输出里能逐一见到:

重定位类型含义计算公式(简化)典型场景
R_X86_64_6464位绝对地址S + A64位绝对地址引用
R_X86_64_PC3232位PC相对偏移S + A - P函数调用、相对跳转
R_X86_64_3232位绝对地址S + A需要绝对地址的mov等操作数
R_X86_64_32S带符号32位绝对地址S + A(进行符号扩展检查)某些带符号操作数
R_X86_64_PLT32通过PLT的PC相对调用PLT地址 + A - P动态库函数调用
R_X86_64_GOTPCREL从GOT取地址GOT表项地址 + A - P访问动态库的全局变量
R_X86_64_GOTPC64计算GOT自身地址GOT地址 + A - P位置无关代码取GOT基址

这个表不需要背,但要明白逻辑:无论哪种类型,最终计算的落脚点都是往指令的操作数里填一个值,区别在于这个值的参考基准是什么。绝对地址类填的就是符号的真实地址;PC相对类填的是符号地址与当前指令地址之间的差。

2.4 符号表与节信息的关系

重定位项里那个符号引用不是直接存字符串,而是存一个符号表索引。链接器拿到这个索引后,去符号表里查对应符号的最终地址。符号表里每个符号有:名字、符号值、所在节索引、绑定信息等。链接器对重定位项的处理流程就是:解析符号表 -> 确定符号最终地址 -> 代入重定位类型对应的公式 -> 修正目标位置的机器码。

这里有个细节:符号表里还有一个SHN_ABS类型和SHN_COMMON类型的符号。SHN_ABS符号(比如定义的常量某些汇编符号)通常不要重定位,地址是固定的;SHN_COMMON是未初始化的全局变量占位,它们不放在任何节里,链接时会合并到.bss或.data中。这类符号的重定位计算方式和普通定义符号略有差异,比如COMMON符号在最终分配前地址是未知的,但链接器会统一处理。

提示:如果你在readelf -r输出里看到某个重定位项的符号是“绝对”的(SHN_ABS),通常说明这个符号的地址直接写在指令里,不需要链接器额外修正。反之,普通符号则要参与地址计算。

3. 静态链接的重定位全过程:从符号解析到机器码修正

上面看完了数据结构,现在来手工推演一遍重定位的完整流程。这部分我会讲得很细,因为理解了计算的底层逻辑,你以后看到链接脚本、PIC、延迟绑定等进阶玩法都会轻松很多。

3.1 链接器工作的三个阶段概览

一个经典的观点认为链接器工作分三步:扫描目标文件并建立全局符号表;在此基础上解析所有符号引用(找出每个重定位项引用的符号到底定义在哪);最后修正地址,让代码变成可执行。三个阶段之间有依赖关系,但核心思想是“先解决符号在哪的问题,再解决指令里放的数是多少的问题”。

main.o里引用了external_func,但它的定义在另一个文件funcs.c里。链接器首先要把这两个目标文件的符号表合并成一张全局符号表;接着它看到main.o的重定位项引用external_func,就去这张表里找,找到它在funcs.o的.text节里偏移为0的地方(就是第一个函数),验证类型匹配没问题。接下来要修正main.o里那条调用指令了——这就要知道两个目标文件合并后external_func的最终地址。

3.2 输入节的排列与地址分配

链接器把所有输入目标文件的节按相同名称统一合并到输出文件的节里。比如两个.o文件的.text节首尾相接,合成一个更大的.text节。合并的具体布局由链接脚本决定(ld默认脚本或用户自定义脚本)。排列完成后,每个输入节就有了一个最终的虚拟地址(VMA)。linker script中的:

  • .text起始地址加上该节在合并后节里的偏移 = 该输入节的加载地址
  • 节内所有重定位位置也对应有了绝对地址

举个例子:一个程序里有两个.o文件,一个是main.o的.text节(大小0x20),另一个是funcs.o的.text节(大小0x40)。如果链接脚本里.text VMA从0x400000开始,那么main.o的.text就占据0x400000~0x40001f,funcs.o的.text占据0x400020~0x40005f。也就是说external_func的真实地址大概是0x400020那个位置。

3.3 重定位计算的数学公式推导

现在来算。假设:

  • S = 符号的真实地址(最终解析后)
  • A = addend(重定位项里给的修正量)
  • P = 需要修正的位置的地址
  • GOT/S相关概念先不管

对于R_X86_64_PC32类型,链接器要往目标位置写入的值是 S + A - P。这个是教科书公式。关键在于这个A和P到底是什么含义。

回到我们前面那个例子,main.o里那条引用external_func的指令,假设是E8 call指令(x86-64的call rel32)。这条指令长度为5字节:第一个字节是操作码0xE8,后面4字节是32位相对偏移。在调用指令执行时,CPU会取“下一指令的地址”(也就是P+5)加上这个32位偏移,得到跳转目标。

那编译器在生成这条指令时,由于不知道目标在哪,就先放了一个占位偏移(假设是0x00000000),然后在.rela.text里记录一条重定位项:

  • r_offset = 0x15(操作数在.text节内的偏移,也就是从源码角度看这4字节占位的位置)
  • r_info = 符号external_func + R_X86_64_PC32
  • r_addend = -4

为什么addend是-4?因为占位偏移是0,如果链接器只是简单地把(S - P + A)填进去,而P被定义成“操作数所在位置”也就是那个4字节的起始地址,那CPU在执行时计算的是:当前指令结束地址(P + 4) + 偏移。可我们希望最终效果是P + 4 + 偏移 = S,于是偏移 = S - (P + 4) = (S - P) - 4。所以A = -4就是为了补上这个“指令长度”的差值。

再看P的定义,在ELF规范里P是修正位置的地址,vmlinux等各类链接器脚本里则常常用当前位置来计算。实际就是上面这个逻辑。把A = -4代进去:处理器要填的值 = S - 4 - P。如果P定义为操作数起始地址,这个结果就是我们说的S - (P + 4)。

如果把P定义成“下一指令地址”V,那就变成S - V。两种定义写出来公式不同,但物理意义一样。ELF规范采用P是offset of the place being relocated那种定义,所以我们看到A都是-4。

3.4 用readelf和objdump验证手算过程

光说不练假把式,我拿一个真实的demo来演示。假设有这两个文件:

file1.c

extern int global_counter; void increment(void) { global_counter++; }

file2.c

int global_counter = 10;

编译两个目标文件再链接:

gcc -c file1.c -o file1.o gcc -c file2.c -o file2.o ld file1.o file2.o -o linked_program

看file1.o的重定位表:

readelf -r file1.o

结果里.rela.text节能看到一条R_X86_64_32S类型的重定位(因为global_counter++里的mov操作需要一个绝对32位地址)或R_X86_64_PC32(取决于编译选项和代码生成模式)。我们要关注的是:addend是什么,r_offset在哪里。

然后看可执行文件里对应指令位置,值是多少。拿objdump -d linked_program查看,再对照符号表里global_counter的地址,你就发现那个mov指令里的操作数正好等于符号的最终地址。把这个结论记录下来,和readelf里看到的A对应起来,就能把这个计算过程彻底验证明白。

注意:要不要用-fno-pie编译会影响生成的汇编。默认的Linux发行版开启了PIE,导致一些绝对寻址被编译器改成PC相对方式。建议实验时用gcc -fno-pie -no-pie来避免干扰,得到的结果更直观。

3.5 重定位里容易忽略的细节:addend的符号与溢出检查

静态链接还有个必须注意的检查:写入的修正值必须适配目标字段的宽度。比如R_X86_64_32类型要求32位有符号扩展后代表64位地址,如果你的代码段和数据段被链接器放在1G以上的内存区域(或者4G之外),这个修正值就放不进去了,链接器会报“relocation truncated to fit”错误。

这是实际工作中最容易碰到的重定位报错之一,特别是在嵌入式裸机开发中。比如某芯片的RAM起始地址在0xC0000000以上,你链接脚本把.rodata放到0xC0000000以后的地址,而代码里用R_X86_64_32去引用一个只读字符串常量时,地址0xC0123456就超了32位有符号范围吗?0xC0...最高位是1,作为32位会被当成负数,于是链接器拒绝写入。解决办法就是改用PIC模式、64位绝对寻址、或者调整链接脚本分配地址。

另一个细节:链接器是否允许符号地址是动态分配的(比如PIE可执行文件的符号地址在加载时才知道)。这种情况不能用静态重定位方式修正,必须走动态重定位,我们在下一段讲。

3.6 实操心得:为什么静态库里的.o没有出现在链接输出里

了解了重定位过程,你也能明白一个此前可能很头痛的问题:为什么静态库的链接有时候“查不到符号”?因为静态库被链接时,链接器是贪婪搜索需要的模块。假设你的main.o引用外部符号foo,链接器从左到右扫描库文件,如果碰到一个库成员的符号表里有foo的定义,就把这个成员提取出来参与链接。但是如果这个库文件被放在main.o之前,而当时链接器还不知道需要foo,那它可能就跳过那个成员了,最终导致undefined reference。这就是为什么库文件一般排在源文件后面,且循环依赖的库要用-start-group、-end-group包裹。知道这个过程,你就知道怎么正确设计库的顺序。

4. 动态链接下的重定位:GOT、PLT与延迟绑定的逻辑

如果你制作的程序运行时会加载.so动态库,那重定位的计算会比静态链接复杂一个维度。原因很好理解:动态库的最终加载地址在编译和链接阶段是未知的,甚至同一个动态库在不同进程里可能被加载到不同地址。这时链接器(动态链接器,更准确说是运行时解析器)仍然要负责修正地址,但策略完全不同。

4.1 静态共享库如何在运行时处理地址绑定

在Linux上,动态库在编译时通常加-fPIC(位置无关代码)选项。这个选项的核心目的就是让编译出的代码不依赖绝对地址,从而可以被加载到任意虚拟地址。做法是把对外部符号的引用改为间接引用:不直接引用外部函数或变量,而是引用链接到本模块的GOT(全局偏移表)和PLT(过程链接表)。

你如果反汇编一个加了-fPIC的.so,会看到对动态库外部函数的调用并非直接call目标地址,而是先通过PLT做跳板,PLT再跳到GOT里存的真实函数地址。GOT里的每个条目在初始阶段可能指向PLT里的解析代码,第一次调用时触发动态链接器去解析真实函数地址并写回GOT。这个机制叫延迟绑定(lazy binding),它的产生是为了避免程序刚启动就要做全部符号解析,浪费大量时间。

4.2 动态重定位的类型差异

动态库里的重定位项和静态链接的可执行文件类似,但多了几类特殊的:

  • R_X86_64_GLOB_DAT:修正GOT里的一个条目,让它指向某个全局符号,加载时直接算好
  • R_X86_64_JUMP_SLOT:修正GOT里存储的函数入口地址,配合PLT做跳转(这就是我们常说的PLT重定位)
  • R_X86_64_RELATIVE:把某个位置的值加上加载基址,这个常用于数据段的地址修正

其中R_X86_64_RELATIVE是个很有意思的类型:因为PIC代码里很多地址本身就是相对的,动态链接器只要把加载基址加上这个相对偏移就能得到真实地址。不需要知道符号名,效率很高。

用readelf -d libxxx.so可以查看动态段里的.rela.dyn和.rela.plt。前者存放变量和数据地址修正,后者存放函数跳转表修正。区分清楚这两个表,能帮你排查很多动态库加载异常的问题。

4.3 为什么静态链接到动态库里的“冲突”会发生

动态链接还有一个特点:如果两个不同的动态库都定义了同名符号,动态链接器的符号查找顺序(一些库先被加载,则它的符号优先)可能导致你的代码调用到了错误版本的函数。这本质上也是重定位解析的一种表现:符号被解析到哪,就修正到哪。很多团队用版本控制脚本来避免这种问题。

4.4 PIC代码与非PIC代码的性能和体积对比

我实测过开-ffunction-sections -fdata-sections -fPIC这些选项对代码尺寸的影响。PIC代码因为要多间接一层,指令序列往往更长,但好处是代码段是真正可共享的,且加载地址灵活。如果你想做裸机或内核模块那种绝对定位的代码,千万别开PIC,不然会引入一堆用不到的重定位机制,链接脚本也会更难写。

注意:在嵌入式领域,有些MCU的编译工具链不支持动态链接器,但一样使用重定位。那里的做法更像静态重定位:链接脚本确定最终地址,所有地址一次性修正,且修正后代码和数据不再迁移。这种情况下你用的重定位类型和参数可能和x86-64的海量类型完全不同,比如ARM的R_ARM_ABS32、R_ARM_CALL,不过基本思想——符号地址代入、指令修正——是一样的。

5. 常见重定位错误与排查思路(实战清单)

这一部分最有价值。我在实际开发中踩过不少和重定位相关的坑,列出来供你排查时对照。

症状可能原因排查方法
undefined reference to xxx符号真没定义、定义但未链接进最终程序、库未参与链接nm查看目标文件符号,确认库顺序
relocation truncated to fit32位重定位到超出范围的地址查地址布局,改用PIC/64位重定位/调整脚本
运行时跳转到了错误地址缓存了旧的可执行文件、可执行文件与库不匹配重新构建,检查soname和符号版本
编译可以,运行秒崩动态重定位没解析成功,GOT项是0或无效值用gdb看反汇编,确认GOT/PLT流程
数据访问读到垃圾值数据段有重定位条目未被正确处理检查链接脚本是否覆盖.rela.dyn段

5.1 逐步排查一个undefined reference问题

场景:你的项目里调用了libfoo.a里的函数bar,但链接报错undefined reference to bar。做法是:

  1. 先确认bar是否真的存在于libfoo.a里:用nm libfoo.a | grep ' bar$'看。如果输出里有多个成员文件,确认哪个成员包含它。有时候库里有但符号是局部版本(比如static bar)会导致找不到。
  2. 确认这个成员是否被链接器拉进来。链接器只会在需要某个符号时才提取成员。你用nm查看可执行文件或最终链接的符号表,如果在最终产物里依然没有bar,说明成员没被提取。
  3. 检查库的位置:在gcc命令里,库应该放在引用它的源文件后面。如果你写了gcc main.o -lfoo ...那没问题;但如果是gcc -lfoo main.o,链接器扫描到libfoo.a时还不知道需要bar,就会跳过。
  4. 检查库是否用-Wl,--start-group包住了循环依赖。

5.2 通过readelf和objdump定位重定位错误

我记得有一次在做一个交叉编译的项目,程序跑起来后某个全局变量值总是错的。后来用gdb在启动早期打断点,发现那个变量的地址处存的数值不对。进一步查,问题出在链接脚本没有正确地将一个包含重定位项的输入节放在输出节的对应位置,导致重定位项虽然存在,但没有被动态加载器处理。解决方法是检查链接脚本里是否保留.rela.dyn和.plt段区域。

这种问题最典型的特征就是:编译链接都不报错,但一运行就不对。排查思路要转到“重定位是否存在但没被执行”这个方向,用readelf -r看看你的可执行文件或库的重定位表,然后确认动态加载器有没有机会处理这些表。

5.3 链接脚本相关的重定位注意事项

写自定义链接脚本时,几个易错点直接影响重定位:

  • 不要把含有重定位项的节放在没有地址映射的虚拟段里,否则修正值没地方写。
  • 如果你想把某些节放到特殊地址,确保该地址在各符号的重定位类型可表示的范围内。
  • 链接脚本里定义的PROVIDE符号,如果它指向一个不属于任何输入节的“纯脚本符号”,它的地址可以在脚本里自由设置,但要小心它和实际符号冲突导致计算出奇怪的值。

5.4 动态链接加载期间的“重定位不是预期的”问题

另一种常见问题:你编译了一个.so,另一半程序加载它时突然崩溃,用一个版本的库和另一个版本的调用方链接。这种问题经常不是代码逻辑错误,而是调用方编译时对函数签名或返回值的处理与库内实际实现不匹配。但这种不匹配也能在重定位阶段暴露出来。比如调用方代码里有一条mov指令期望某个符号是函数,但库里的同名符号却是数据对象,重定位进去后,放到指令里就会崩。我在前面那个表里提到的“编译可以运行秒崩”很多时候就是基于这种场景。

出现这种情况时,我建议查一下调用方和被调方是否都使用了相同头文件、相同ABI约定,以及是否有两个版本同名库同时在LD_LIBRARY_PATH中。用LD_DEBUG=bindings、LD_DEBUG=reloc可以打印动态链接器做重定位的过程,定位问题非常有效。

5.5 实测经验:从报错信息直接指向问题根因,少走弯路

很多新手看到一大串链接报错会慌,其实报错本身已经把方向指出来了:

  • 如果报错里有“undefined reference”,优先检查符号本身和库顺序。
  • 如果报错里有“relocation truncated to fit”,优先检查目标地址范围和重定位类型。
  • 如果报错里有“shared object file build error”或者“cannot open shared object file”,大概率是运行时动态库路径问题,不是重定位本身的错误。
  • 如果你看到“hidden symbol ... is referenced by DSO”,这通常是因为一个库试图引用另一个库里的非导出符号,动态重定位不允许这种跨DSO的引用——排查目标就是符号的可见性属性(default vs hidden)。

这些经验都有个共同点:重定位本质是符号地址的解析过程,所以几乎所有错误都指向“符号-地址-库”三者之间的不匹配。搞清楚符号定义在哪、库路径对不对、地址空间够不够,90%的重定位问题都能自己解决。

6. 对重定位更深一层的理解与实操技巧

前面五节已经覆盖了从理论到实操的大多数内容。最后我再额外聊几个平时可能不会注意到、但一旦遇到就很关键的细节。这些内容是我在几次实战里踩坑后总结出来的,作为一个资深码农,我觉得比背教科书公式更有价值。

6.1 用编译选项控制重定位的生成和优化

GCC有若干影响重定位生成的选项,每个背后都牵涉到一套不同的链接处理方式:

  • -fno-plt:让函数调用不经过PLT,而是直接从GOT读取目标地址并跳转,减少了跳板层,也减少了动态符号解析的延迟。代价是GOT条目必须在加载时全部分析完成,失去了延迟绑定的省时效果。
  • -fvisibility=hidden:默认把所有符号设为隐藏,必须在源码里显式标记要导出的符号。这能大幅减少动态库导出的符号数量,从而减少动态重定位项的数量,还能避免同名符号冲突。产品化的动态库我建议都打开这个选项。
  • -Wl,-Bsymbolic:链接动态库时让库内部的符号引用优先绑定到库自身定义的符号,而不是等动态加载器去外面找同名符号。这个选项能降低符号被外部覆盖的风险,但也要注意它可能破坏一些预期中的覆盖机制。
  • -ffunction-sections -fdata-sections配合-Wl,--gc-sections:把每个函数和全局数据放到独立节里,链接时回收没被引用的节。这对减小最终文件体积有帮助,但要注意如果一个节里有重定位项而它引用的目标被回收了,链接器会报错。

6.2 动态链接器如何处理加载时的重定位

Linux下动态链接器是ld-linux.so,它的工作流程从内到外大致是:解析动态段,建立全局符号表;对每个需要重定位的表做递归处理;之后才把控制权交给程序的入口。你可以用环境变量LD_DEBUG=reloc,bindings,libs来观察这个过程中的每一步。我遇到过一个库在加载时崩溃,用LD_DEBUG=reloc看到某个重定位项的引用符号始终解析为0地址,于是立刻判断是那个符号没有被找到或类型不匹配。这种调试手段比逐行读汇编高效得多。

6.3 一个反向工程视角:重定位表在逆向分析中的作用

稍微提一个有趣的应用方向。逆向分析一个二进制时,看它的重定位表可以直接定位哪些指令引用了外部符号,哪些数据指向了动态库里的条目。对未strip的可执行文件,你甚至可以只根据重定位表把程序里“哪些函数是外部调用、哪些是内部调用”分类出来。这也是为什么很多加壳或混淆工具会先处理重定位表或动态符号信息,因为它们是分析程序结构的捷径。

6.4 静态与动态混合链接的实战考察

实际项目很少是纯静态或纯动态,往往是混合链接:你的一部分代码静态链接进可执行文件,另一部分通过动态库解析。这时候要注意重定位在不同模块之间的切换。静态链接进来的那些.o的重定位项由静态链接器在最终链接时修正;动态库里的可以由动态加载器在加载时修正。两者处理的对象不同,但最终目的都是让指令里出现的地址在运行时可被正确计算。

这也就是为什么链接命令里关于库顺序、-rpath、-soname等等一个个坑都会冒出来。理解重定位后你再回头看动态链接那一整套机制,就像拿到了一张地图,所有符号地址解析、延迟绑定、GOT和PLT都不再是神秘的黑盒。

给初次接触重定位的读者的几点建议

如果你刚开始接触链接和重定位,我的建议是从小实验做起:找两个最简单的C文件,用gcc -c分别编译,再用ld把它们链接起来,然后用readelf、objdump逐步观察整个过程。不要只停留在理论层面;一旦你把某一条call指令的修正过程用手算验证一遍,重定位这个概念就再也不会忘记了。

我在实际工作中还有一个习惯:每次拿到新的链接脚本或构建系统,第一件事就是打开生成的.map文件,看看各节的地址分布和符号地址是否符合预期。这种习惯帮我早早发现过很多重定位隐患,省下了大量排查时间。希望这篇文章能让你少走弯路,如有不同看法,欢迎讨论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 21:21:01

HTML5静态购物网站带登录注册与购物车完整实现教程

简介:这是一套面向前端初学者与进阶学习者的HTML5静态购物网站完整源码,围绕登录注册、商品展示、详情页与购物车等核心模块展开,帮助读者理解如何用HTML5、CSS3、JavaScript与jQuery搭建一个可运行的电商前端原型。压缩包共48个文件&#xf…

作者头像 李华
网站建设 2026/10/18 21:11:15

Dify本地部署Chrome MCP实战:3分钟搭建网页自动化AI助手

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 21:10:45

MATLAB复现HANK模型:从离散化到脉冲响应的完整链路

简介:这份资源面向宏观经济学研究者、高年级研究生及具备一定MATLAB基础的量化分析人员,提供异构代理新凯恩斯(HANK)模型的完整复制代码,用于在MATLAB环境中搭建、求解并模拟包含家庭与企业异质性的动态随机一般均衡框…

作者头像 李华
网站建设 2026/10/18 21:10:38

深度特征映射语音增强实战:从STFT特征到全连接网络训练全解析

简介:一套基于深度特征映射的语音增强工程实现,面向计算机专业学生与开发者,可支撑毕业设计、课程实训及公司项目二次开发。整套资源共一百四十四个文件,约五十七点八一兆字节,核心包含四十三个Python脚本、三十八个TX…

作者头像 李华