摘要:本篇主要介绍在静态链接中多个文件合并、地址确定、符号解析和重定位相关问题,以GCC编译器为例。
首先,链接器链接多个文件时,采用何种方式合并为一个文件?方式一,按序叠加,即多个文件依次叠加起来;方式二,相似段合并。采用何种方式就要看哪种方式利大于弊。
方式一:这种方式实现简单,链接速度快,基本不需要太多操作。但是,通常简单的东西往往是粗暴的。我们知道gcc编译后得到的可重定位目标文件是由各种段(section)组成的,这样简单叠加会产生大量零散的段,项目越大这样的段越多,而且还是大量相同名称的段。并且由于每个段都有地址和空间的对齐要求,这样做势必会浪费大量的内存空间(内部碎片)。所以这种方案并不好,可谓小甜头换大痛苦。
方式二:这种方式是将相似段合并,比如将多个不同文件的.text段合并为一个大.text段,类似的.data、.bss等等也是如此。最终得到的文件,在段的数量和类型上与原来各个小文件没有大的区别,只是每个段的大小变大了。当然这样做实现细节上肯定要复杂,也会牺牲一定的速度。但是这种付出是值得的。
对应方式二这种方式链接器一般采用两步链接,即分两步走。
空间与地址分配:(1)扫描各个输入文件获得各个段的长度、属性、位置等信息;(2)收集各个文件的符号表并建立统一的全局符号表。这一步将根据各个段的信息计算出合并后各个段的长度和位置,并建立映射关系(我的理解是更新段表的信息,段表描述ELF文件包含的所有段的信息,比如段名、长度、偏移量等)。
符号解析与重定位:这一步至关重要,由于上一步对相似段进行合并之后,原先的符号表信息已经过时,并且原先文件中代码的地址并没有映射到虚拟地址空间中,所以这一步要完成符号解析与重定位、调整代码中的地址等。
下面是对上面第二步的进一步解析。
首先调整代码位置这相对来说比较简单也易于理解,以
Linux为例,在Linux下32bit ELF可执行文件默认地址从0x08048000开始分配,根据合并后各个段的位置做相对移位即可。如有下面示例:
代码b.c
1 int shared = 1;
2 void swap(int* a, int* b)
3 {
4 *a ^= *b ^= *a ^= *b;
5 }
代码a.c
复制代码
1 extern int shared;
2 int main(int argc, char** argv)
3 {
4 int a = 100;
5 swap(&a, &shared);
6 return 0;
7 }
8 ~
复制代码
编译后输出a.o,b.o,cc -c a.c b.c,使用objdump查看a.o、b.o如下:
连接a.o b.o,的到可执行文件ab
可以看到a.o和b.o他们的起始地址都为0,而可执行文件ab的起始地址则是从0x0804000起(.text段之前还有文件头)。
重点和难点在于符号解析和重定位,即要更新文件合并后的总全局符号表,在构建全局符号表时即完成符号解析,重定位需要在符号解析后完成。在目标文件的结构中有一种section叫重定位表,各个段中如果有需要重定位的符号那么就会有相应的重定位表,如.text的重定位表是.rel.text。由于在a.c代码中用到了shared和swap这两个符号都属于b.c文件中的定义,链接时需要重定位,所以a.o中的的text段就会有相应的重定位表。同样用objdump可以查看目标文件的重定位表内容:
我们可以看到有两行是关于需要重定位符号shared和swap的描述,其中OFFSET表示它们在a.o文件中的偏移值,TYPE表示重定位时对指令的修正方式,下面是书中对其解释的相应的图表: