GCC 静态链接过程中的【重定位】过程分析

📅 发布时间:2026/7/4 12:32:13 👁️ 浏览次数:
GCC 静态链接过程中的【重定位】过程分析
关注星标公众号不错过精彩内容来源 | IOT物联网小镇最近因为项目上的需要利用动态链接库来实现一个插件系统顺便就复习了一下关于Linux中一些编译、链接相关的内容。在链接的过程中符号重定位是比较麻烦的事情特别是在动态链接的过程中因为需要考虑到很多不同的情况。这篇文章作为第一篇先来聊一聊静态链接中的重定位过程。按照惯例还是以一个简短的示例代码作为载体看一看GCC在链接的过程中是如何根据目标文件(.o文件)来进行重定位生成最终的可执行文件的。示例代码示例代码很简单一共有2个源文件main.c和sub.c。在sub.c中定义了一个全局变量和一个全局函数然后在main.c中使用这个全局变量和全局函数。代码如下sub.cmain.c在一般的开发过程中都是使用GCC工具直接把这2个源文件编译得到可执行文件。但是为了探究编译、链接过程中的一些内部情况我们需要把编译、链接的过程拆开从中间过程中产生的目标文件(.o 文件)中来查看一些详细信息。先把这2个源文件编译成目标文件sub.o和main.o:$ gcc -m32 -c sub.c $ gcc -m32 -c main.c这样就得到了两个目标文件先来初步看一下这2个目标文件中的一些信息。以上这两个编译过程是各自独立的虽然main.o中使用了两个符号(全局变量和全局函数)但是此时main.o并不知道这2个符号是在哪个文件中定义的。当链接器把所有的.o文件链接成可执行文件的过程中才能确定这2个符号是在哪里。在Linux系统中目标文件(.o) 和可执行文件都是ELF格式的因此如何查看ELF格式文件的一些工具指令就非常有帮助。很久之前总结过这篇文章《Linux系统中编译、链接的基石-ELF文件扒开它的层层外衣从字节码的粒度来探索》里面详细总结了ELF文件的内部结构以及一些相关的工具。sub.o 文件内容分析段信息首先来简单瞄一眼一下sub.o中的一些信息。sub.o中的段信息如下(指令$ readelf -S sub.o)我们主要关心黄色的代码段和数据段就可以了可以看出:代码段(.text)地址Addr是 0x0000_0000(因为这是目标文件不是可执行文件所以不会安排地址)它在 sub.o 文件中的偏移量(Off)是 0x34长度是 0x0C 字节;数据段(.data)地址Addr是 0x0000_0000它在 sub.o 文件中的偏移量(Off)是 0x40长度是 0x04 字节;简单算一下sub.o的开始部分是ELF的header通过readelf -h sub.o指令可以看出来header部分是52个字节(即0x34)如下因此可以得到代码段(.text)是紧接在 header 之后长度是 0x0C 个字节在文件中占据着 0x34 ~ 0x3F 这部分空间(0x3F 0x34 0x0C - 1);数据段(.data)是进阶在代码段之后在文件中占据着 0x40 ~ 0x43 这部分空间;符号表信息下面再来说说符号表的事情。简单来说符号表就是一个文件中定义的所有符号、引用的外部符号(在其它文件中定义)包括变量名、函数名、段名等等都属于符号。当然了在ELF文件中会详细的说明每一个符号的类型、大小、可见性等信息。如果对ELF文件格式有过了解的话一定知道每一条符号信息都是通过一个结构体来描述具体含义的描述符号表的结构体如下// Symbol table entries for ELF32. struct Elf32_Sym { Elf32_Word st_name; // Symbol name (index into string table) Elf32_Addr st_value; // Value or address associated with the symbol Elf32_Word st_size; // Size of the symbol unsigned char st_info; // Symbols type and binding attributes unsigned char st_other; // Must be zero; reserved Elf32_Half st_shndx; // Which section (header table index) its defined in };再来看一下sub.o中的符号表下面这张图(指令readelf -s sub.o)关注上图中黄色矩形中的两个符号SubData和SubFunc很明显它们就是sub.c中定义的两个符号全局变量和全局函数。对于SubData符号来说Size4: 长度是 4 个字节;TypeOBJECT说明这是一个数据对象;BindGLOBAL说明这个符号是全局可见的也就是在其他文件中可以使用;Ndx2说明这个符号是属于第 2 个 段中就是数据段(.data);同样的道理对于SubFunc符号来说Size12: 长度是 12 个字节;TypeFUNC说明这是一个函数;BindGLOBAL说明这个符号是全局可见的也就是在其他文件中可以调用;Ndx1说明这个符号是属于第 1 个 段中就是代码段(.text);main.o 文件分析按照上面的步骤把main.o中的这几个信息也查看一下。段信息指令readelf -S main.o可以看出:代码段(.text)地址Addr是 0x0000_0000(因为这是目标文件不是可执行文件所以不会安排地址)它在 sub.o 文件中的偏移量(Off)是 0x34长度是 0x32 字节;数据段(.data)地址Addr是 0x0000_0000它在 sub.o 文件中的偏移量(Off)是 0x66长度是 0 个字节因为它没有定义变量;在文件中的布局如下所示符号表信息指令readelf -s main.o重点看一下黄色矩形中的3个符号。main符号Size50: 长度是 30 个字节也就对应着代码段的长度 0x32 ;TypeFUNC说明这是一个函数;BindGLOBAL说明这个符号是全局可见的也就是在其他文件中可以调用;Ndx1说明这个符号是属于第 1 个 段中就是代码段(.text);下面两个符号SubData和SubFunc他们的Ndx都是UND表示这2个符号被main.o使用但是定义在其他文件中。我们知道当链接成可执行文件时所有的符号都必须有确定的地址(虚拟地址)所以链接器就需要在链接的过程中找到这2个符号在可执行文件中的地址然后把这两个地址填写到main的代码段中。可以先来看一下main.o的反汇编代码指令objdump -d main.o黄色矩形框中是把数值0存储到eax寄存器中然后把eax压到栈中然后红色矩形框调用了一个函数。从示例代码(.c文件)中可知main函数在调用sub.c中的SubFunc函数时传入了变量SubData。黄色部分的00 00 00 00就应该是符号SubData的地址只不过此时main.o还不知道这个符号的将会被链接器安排在什么地址所以只能空着(以4个字节的00来占位)。红色部分的调用(call)地址为什么是fc ff ff ff?按照小端格式计算一下0xfffffffc十进制的值就是-4为什么设置成-4呢对于x86平台的ELF格式来说对地址进行修正的方式有2种绝对寻址和相对寻址。绝对寻址对于SubData符号就是绝对寻址在链接成可执行文件时这个地址在代码段中偏移0x12个字节(黄色矩形框指令码偏移0x11个字节跨过一个字节的指令码a1就是0x12个字节)这个地方4个字节的当前值是00 00 00 00。链接器在修正的时候(就是链接成可执行文件的时候)会把这4个字节修改为SubData变量在可执行文件中的实际地址(虚拟地址)。相对寻址红色矩形框中的函数调用(SubFunc符号)就是相对寻址就是说当CPU执行到这条指令的时候把PC寄存中的值加上这个偏移地址就是被调用对象的实际地址。链接器在重定位的时候目的就是计算出相对地址然后替换掉fc ff ff ff这四个字节。PC寄存器中的值是确定的当call这条指令被CPU取到之后PC寄存器被自动增加指向下一条指令的开始地址(偏移0x1f地址处)。实际地址 PC值 xxxx_xxxx所以得到xxxx_xxxx 实际地址 - PC值。而PC值与xxxx_xxxx所在的地址之间是有关系的PC值 (-4)就得到xxxx_xxxx所在的地址因此在main.o中预先在这个地址处填fc ff ff ff-4。问题来了链接器怎么知道main.o中代码段的这两个地方需要进行地址修正这就是下面介绍的重定位表的作用了重定位表信息指令objdump -r main.o重定位表就表示: 该目标文件中有哪些符号需要在链接的时候进行地址重定位。从图中黄色矩形框可以看出main.o中代码段(.text)的SubData和SubFunc这 2 个符号都需要链接器对它进行重定位。TYPE列R_386_32表示绝对寻址R_386_PC32表示相对寻址;OFFSET列表示需要重定位的符号在main.o文件代码段中的偏移位置。刚才已经看了main.o的反汇编代码可以看到偏移0x12 和 0x1b的地方就是需要进行地址重定位的两个符号。可执行程序 main有了 2 个目标文件sub.o和main.o就可以链接得到可执行程序了$ ld -m elf_i386 main.o sub.o -e main -o main段信息使用readelf工具来看一下main可执行文件中的段信息指令readelf -S main)红色矩形框是代码段(.text)链接器把它放在虚拟地址 0x0804_8094;黄色矩形框是数据段(.data)链接器把它放在虚拟地址 0x0804_9138;从段信息中可以看到main文件中代码段和数据段的布局如下可执行程序main是由main.o和sub.o这两个目标文件组成的所以main中的代码段是由main.o中的代码段和sub.o中的代码段组合得到的对于数据段由于main.o中数据段的长度为0所以main中的数据段就是sub.o中的数据段(长度为4)如下图所示符号表信息指令readelf -s main黄色矩形框中的SubData属于数据段长度是 4 个字节虚拟地址是0x0804_9138与段信息中的值是一致的。红色矩形框中的SubFunc属于代码段长度是 12 个字节虚拟地址是0x0804_80c6。因为main中的代码段包括 2 部分内容main.o 中的代码段 main 函数;sub.o 中的代码段 SubFunc 函数;所以可执行文件main中的代码段先存放的是main函数虚拟地址0x0804_8094长度是0x32(50 个字节)紧接着存放的是SubFunc函数虚拟地址0x0804_80c6长度是0x0c12 个字节。如下图所示链接器在第一遍扫描所有的目标文件时把所有相同类型的段进行合并安排到相应的虚拟地址如上图所示。所谓的安排虚拟地址就是指定这块内容被加载到虚拟内存的什么地方。当可执行文件被执行的时候加载器就把每一块内容复制到虚拟内存相应的地址处。同时链接器还会建立一个全局符号表把每一个目标文件中的符号信息都复制到这个全局符号表中。对于我们的实例程序全局符号表中包括SubData: 属于 sub.o 文件数据段安排在虚拟地址 0x0804_9138;SubFunc: 属于 sub.o 文件代码段安排在虚拟地址 0x0804_80c6;其它符号信息...绝对地址重定位然后链接器第二遍扫描所有的目标文件检查哪些目标文件中的符号需要进行重定位。对于我们的示例程序首先来看一下main.o中使用的外部变量SubData的重定位。从main.o的重定位表中可知SubData符号需要进行重定位需要把这个符号在执行时刻的绝对寻址(虚拟地址)写入到main可执行文件中代码段中偏移0x12字节处。也就是说需要解决2 个问题需要计算出在执行文件 main 中的什么位置来填写绝对地址(虚拟地址);填写的绝对地址(虚拟地址)的值是多少;首先来解决第一个问题。从可执行文件的段表中可以看出目标文件main.o和sub.o中的代码段被存放到可执行文件main中代码段的开始位置先放main.o代码段再放sub.o代码段。代码段的开始地址距离文件开始的偏移量是0x94再加上偏移量0x12结果就是0xa6。也就是说需要在main文件中偏移0xa6处填入SubData在执行时刻的绝对地址(虚拟地址)。再来解决第二个问题。链接器从全局符号表中发现SubData符号属于sub.o文件已经被安排在虚拟地址0x0804_9138处因此只需要把0x0804_9138填写到可执行文件main中偏移0xa6的地方。我们来读取main文件验证一下这个位置处的虚拟地址是否正确指令od -Ax -t x1 -j 166 -N 4 main-Ax: 显示地址的时候用十六进制来表示。如果使用 -Ad意思就是用十进制来显示地址;-t -x1: 显示字节码内容的时候使用十六进制(x)每次显示一个字节(1);-j 166: 跨过 166 个字节(十六进制 0xa6);-N 4只需要读取 4 个字节;注意显示的是小端格式。相对地址重定位从上面描述的重定位表中看出main.o代码段中的SubFunc符号也需要重定位而且是相对寻址。链接器需要把SunFunc符号在执行时刻的绝对地址(虚拟地址)减去call指令的下一条指令(PC 寄存器) 之后的差值填写到执行文件main中的main.o代码段偏移0x1b的地方。同样的道理需要解决2 个问题需要计算出在执行文件 main 中的什么位置来填写相对地址;填写的相对地址的值是多少;首先来解决第一个问题。从main.o的重定位表中可知需要修正的位置距离main.o中代码段的偏移量是0x1b字节。可执行文件main中代码段的开始地址距离文件开始的偏移量是0x94再加上偏移量0x1b就是0xaf。也就是说需要在main文件中0xaf偏移处填入一个相对地址这个相对地址的值就是SubFunc在执行时刻的绝对地址(虚拟地址)、距离call指令的下一条指令的偏移量。再来解决第二个问题。链接器在第一遍扫描的时候已经把sub.o中的符号SubFunc记录到全局符号表中了知道SubFunc函数被安排在虚拟地址0x0804_80c6的地方。但是不能把这个绝对地址直接填写进去因为 call指令需要的是相对地址(偏移地址)。链接器把main代码段起始位置安排在0x0804_8094那么偏移0x1b处的虚拟地址就是0x0804_80af然后还需要再跨过4个字节(因为执行call指令时PC的值自动增加到下一条指令的开始地址)才是此刻PC寄存器的值即0x0804_80b3如下图中红色部分两个虚拟地址都知道了计算一下差值就可以了0x0804_80c6 - 0x0804_80b3 0x13。也就是说在可执行文件main中偏移为0xaf的地方填入相对地址0x0000_0013就完成了SubFunc符号的重定位。还是用od指令来读取main文件的内容来验证一下指令od -Ax -t x1 -j 175 -N 4 main总结经过以上两个重定位操作main.c中使用的两个外部符号就解决了地址重定位问题。再来看一下可执行文件main的反汇编代码从黄色和红色的矩形框可以看出二进制指令中的地址值与上面的分析是一致的。以上就是静态链接过程中地址重定位的基本过程与动态链接相比静态链接还是相对简单很多。------------END------------下血本拆解一款国产ARM工控机竟藏着这么多工业设计巧思分享一个MCU代码自检和诊断的案例8位MCU将会消失吗