当咱们有两个方针文件时咱们如何将它们链接起来构成可执行文件?这个进程中发生了什么?咱们将运用下面两段源代码打开分析:

/* a.c */
extern int shared;
int main()
{
    int a = 100;
    swap(&a, &shared);
}
/* b.c */
int shared = 1;
void swap(int *a, int *b)
{
    *a ^= *b ^= *a ^= *b;
}

运用gcc -c 编译得到两个方针文件a.o 和b.o,a 里面引证到了b 里面的swapshared。接下来咱们就要把a 和b 两个方针文件链接成可执行文件ab。

空间与地址分配

关于链接器来说,整个衔接进程中,便是将方针文件的各个段合并到可执行文件的各个段中, 那么方针文件中各个段时怎么合并的呢?

按序叠加

最简略的方案便是将方针文件的段按照次序叠加起来。

《程序员的自我修养》-(4)静态链接

可是这样做会十分糟蹋空间。假如有成百上千个这样的段,每个段都需求有必定的地址空间和对齐的要求,就会构成内存空间大量的内部碎片。

类似段合并

一个更实践的做法便是将相同性质的段合并到一同。

《程序员的自我修养》-(4)静态链接

链接器为方针文件分配地址和空间,这儿地址和空间有两个含义:

  • 输出可执行文件中的空间

  • 装载后虚拟地址中的虚拟地址空间

关于有实践数据的段,比方.text .data 来说,它在文件中和虚拟地址中都要分配空间。而关于.bbs 段来说分配空间只局限于虚拟地址空间,它在文件中并没有内容。

现在的链接器空间分配战略基本上都是采用类似段合并,这种战略一般都采用两步链接(Two-pass Linking) 的方法。也便是说衔接进程分为下面两步。

空间与地址分配

扫描一切的输入方针文件,获得各个段的长度、属性、方位,并将方针文件中一切符号界说和符号引证搜集,统一放到大局符号表。这样链接器能够计算出输出文件中各个段合并后的长度和方位,并树立暗射联系。

符号解析与重定位

这一步是链接进程的核心,特别是重定位进程。咱们运用链接器将输入文件链接起来$ld a.o b.o -e main -o ab,并运用objdump 检查衔接后地址分配状况。

《程序员的自我修养》-(4)静态链接

咱们能够看到在链接之前VMA(虚拟地址 Virtual Memory Address)都是0,由于虚拟空间还没被分配。链接之后各个段都被分配到了相应的虚拟地址。

符号地址的确认

在上面的扫描和空间地址分配阶段,这个时候输入文件中各个段在衔接后的虚拟地址已经就确认了。由于各个符号在段内的相对方位是固定的,可是链接器需求给每个符号加上一个偏移量,使符号能够调整到正确的虚拟地址。

符号解析与重定位

重定位

咱们先用指令$objdump -d a.o 看下方针文件a.o 的反汇编成果:

《程序员的自我修养》-(4)静态链接

由于是方针文件所以还未进行空间分配,方针文件代码段中开端地址为0x00000000,等空间分配完结以后,各个函数才会确认自己在虚拟空间中的方位。

咱们已用粗体符号出了两个引证sharedswap 的方位。在a.c 源码在编译时成方针文件时,编译器并不知道sharedswap 的地址,由于它们被界说在其他方针文件中,所以编译器暂时吧地址0看作是shared的地址。而swap前面的0xe8是操作码,这条指令是近址相对位移调用指令(Call near,relative,displacement relative to next instruction) 在没有重定位之前,相对偏移被置为0xFFFFFFFC(小端),它是常量-4 的补码形式。这条指令call 的下一条指令为add,相关于add 指令偏移-4 的地址为0x2b – 4 = 0x27。所以这条指令的实践调用地址为0x27。咱们能够看到0x27 存放的是0xFFFFFFFC并不是swap 的函数地址。这也是个假地址。

咱们在通过指令$objdump -d ab 检查可执行文件ab 反汇编的代码段:

《程序员的自我修养》-(4)静态链接

咱们能够看到两个需求重定位的符号都已经被批改了,shared 的地址的确是0x08049108,再来看下swapcall 指令下一条指令为add 地址是0x080480bf,所以相关于add 指令偏移0x00000009 的地址为0x080480bf + 9 = 0x080480c8 刚好是swap 的函数地址。

重定位表

上篇文章提到过重定位相关的信息保存在重定位表(Relocation Table) 中,它是方针文件中的一个或者多个段。比方.text 段有需求被重定位的当地,那么会有一个相对应的段.rel.text 保存.text 段的重定位信息。

咱们用指令$objdump -r a.o 检查a.o 的重定位表:

《程序员的自我修养》-(4)静态链接

RELOCATION RECORDS FOR [.text] 表明这个重定位表是.text 段的重定位表,每个要被重定位的当地叫做一个重定位进口(Relocation Entry),咱们能够看到a.o 中有两个重定位进口。重定位进口的偏移(Offset) 表明该进口在被重定位段中(这儿也便是指.text段)的方位。

符号解析

咱们直接运用指令$ld a.o 来链接a.o,而不将b.o 作为输入:

《程序员的自我修养》-(4)静态链接

这时咱们发现有两符号没有被界说,无法完结链接。

重定位的进程也伴随着符号解析的进程,每个方针文件都可能界说一些符号,或者引证其他方针文件界说的符号。链接器会查找由一切方针文件符号表组成的大局符号表,找到相应的符号后进行重定位。

指令批改方法

不同处理器指令寻址的方法也不同,可是关于32位x86平台下的ELF文件的重定位进口所批改的指令寻址方法只要两种:

宏界说 重定位批改方法
R_386_32 1 肯定寻址批改 S + A
R_386_PC32 2 相对寻址批改 S + A – P
  • A = 保存在被批改方位的值

  • P = 被批改的方位(相关于段开端的偏移量或者虚拟地址)

  • S = 符号的真实地址,即由r_info 高24位指定的符号的实践地址

咱们假设a.o 和b.o 衔接成可执行文件后main 函数的虚拟地址为0x1000,swap 函数的虚拟地址为0x2000,shared 变量的虚拟地址为0x3000。

肯定寻址批改

上文图中能够看到,a.o 的第一个重定位进口即偏移为0x18的mov指令的的批改方法为R_386_32,它批改后的成果应该是S + A。

  • S 是符号shared 的实践地址,即0x3000。

  • A 是被批改方位的值,即0x00000000。(上文方针文件a.o 反汇编代码)

所以这个重定位进口批改后地址为:0x3000 + 0x00000000 = 0x3000。

《程序员的自我修养》-(4)静态链接

相对寻址批改

a.o 第二个重定位进口即偏移为0x26 这条call 指令的批改方法为R_386_PC32,它批改的成果应该是 S + A – P。

  • S 是符号swap 的实践地址,即0x2000

  • A 是被批改方位的值,即0xFFFFFFFC(-4)

  • P 为被批改的方位,当链接成可执行文件时,这个值应该是被批改方位的虚拟地址,即0x1000 + 0x27。

所以这个重定位进口批改后的地址为:0x2000 + (-4) – (0x1000 + 0x27) = 0xFD5。这条相对位移调用指令调用的地址是下一条指令的开端地址加上偏移量,即:0x102b + 0xfd5 = 0x2000,刚好是swap 函数地址。

《程序员的自我修养》-(4)静态链接

从这两个比方介意看出,肯定寻址批改和相对寻址批改的差异便是肯定寻址批改后的地址为该符号的真实地址,相对寻址批改后的地址为符号间隔被批改方位的地址差

COMMON 块

链接器自身并不支持符号的类型,它只知道符号的名字并不知道类型。当界说多个弱符号但类型不一致时首要分三种状况:

  • 两个或两个以上强符号类型不一致

  • 有一个是强符号其他都是弱符号,出现类型不一致

  • 两个或两个以上弱符号类型不一致

第一种状况无需处理,由于多个强符号界说自身便是违法的。链接器首要处理后两种状况。编译器和链接器都支持一种叫COMMON块(Common Block) 机制,当不同的方针文件需求的COMMON 空间巨细不一致时,以最大的为准。所以关于多个弱符号类型不一致时,以占用空间最大的为准。可是假如其间一个为强符号时,那么终究输出成果符号所占用的空间与强符号相同。

所以为什么编译器不直接把未初始化的大局变量也当作未初始化的部分静态变量一样处理,为它在BBS 段分配空间,而是符号为一个COMMON 类型呢?由于编译器编译时弱符号(未初始化的大局变量便是典型的弱符号)终究所占的空间是不知道的,可能在其他编译单元中该符号所占的空间更大。由于所需空间巨细不知道,所以编译器无法为该符号在BBS 段分配空间,可是在链接时能够确认弱符号巨细,所以能够在终究输出文件中的BBS 段为其分配空间。所以总体来看,未初始化大局变量终究仍是被放在BBS 段的。

静态库链接

一个静态库能够简略看成一组方针文件的集合,即许多方针文件通过压缩打包后构成的一个文件。

比方咱们的源代码运用了静态库中的函数,在链接时链接器会去静态库中查找运用到的符号,把符号地点的方针文件链接。而这个方针文件很有可能还以来其他方针文件。幸好链接器会处理这些繁琐的事务,链接器会主动寻觅一切需求的符号及它们地点的方针文件,将这些方针文件从静态库中解压出来,终究衔接在一同成为一个可执行文件。

静态库里一般一个方针文件只包含一个函数,由于链接器在链接静态库文件的时候以方针文件为单位的。假如有许多函数放在一个方针文件中,很有可能将许多没用的函数一同链接进了输出成果中,这样会导致空间糟蹋。

引证

程序员的自我涵养