静态链接

空间与地址分配

对于多个输入目标文件,链接器如何将它们的各个段合并到输出文件?或者说,输出文件中的空间如何分配给输入文件?

按序叠加

一个最简单的方法就是将输入的目标文件按照次序叠加起来,也就是将各个目标文件依次合并。但这样做会造成一个问题,在有很多输入文件的情况下,输出文件将会有很多零散的段。就会有很多零散的段,因为每个段是有一定的地址和空间对齐要求,这样会造成内存空间大量的内部碎片。所以这不是一个好办法。

相似段合并

一个更加实际的办法是将相同性质的段合并到一起,比如将所有输入文件的“.text”合并到一起,其他段也是一样的。

”链接器为目标文件分配地址和空间“这句话中的“地址和空间”其实有两个含义:第一个是在输出的可执行文件中的空间;第二个是在装载后的虚拟地址中的虚拟地址空间。 事实上,我们在这里谈到的空间分配只关注于虚拟地址空间的分配。因为这关系到链接器后面的关于地址计算的步骤,而可执行文件本身的空间分配与链接过程关系不大。

现在链接器空间分配的策略基本上都是采用相似段合并的方法。使用这种方法的链接器一般都采用一种叫 两步链接的方法。也就是说整个链接过程分两步:

  • 第一步 空间与地址分配 扫描所有的输入目标文件,获得它们各个段的长度,属性和位置,并且将输入目标文件中的符号表中所有符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的段长度,并且将他们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。
  • 第二步 符号解析与重定位 使用上面第一步收集到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符号解析与重定位,调整代码中的地址等。事实上,第二步是核心,特别是重定位

我们使用ld链接器将”a.o”与”b.o”链接起来:

1
$ld a.o b.o -e main -o ab 

因为现在的机型大部分是64位,我们这里分析32位的文件,应该用以下命令编译和链接

1
2
$gcc -c -fno-stack-protector a.c b.c  -m32
$ld -e main -o ab -T tes1.lds --oformat elf32-i386 a.o b.o

ld命令后缀

  • -e main 表示将main函数作为程序的入口,ld链接器默认的程序入口为_start。
  • -o ab 表示将链接输出文件命名为ab,默认为a.out
  • -T 使用tes1.lds 作为链接器脚本文件,这里是我们根据默认的链接器脚本文件更改了架构的文件。
  • –oformat 指定输出文件的格式为elf32-i386,这里用的是bfd库里面的名称。

gcc命令后缀

  • -c 编译为目标文件,也就是可重定位文件
  • –fno-stack-protector 关闭文件的Stack Canaries 保护,也就是栈溢出保护,默认是开启的。
  • -m32 编译为32位的文件

我们可以用objdump来查看链接前后的地址分配情况:

1
$objdump -h a.o b.o ab 

VMA表示Virtual Memory Address ,即虚拟地址,LMA表示Load Memory Address,即加载地址,正常情况下这两个值应该是一样的,但是在有些嵌入式系统中,特别是那些程序放在ROM的系统中时,LMA和VMA是不相同的。一般我们看VMA即可。

链接前后的程序中所使用的地址是程序在进程中的虚拟地址,即我们关心上面各个段中的 VMA(Virtual Memory Address)Size,而忽略 文件偏移(File off)。我们可以看到,在链接之前,目标文件中的所有段的VMA都是0,因为虚拟空间还没有分配,所以他们默认都是0。链接之后,可执行文件中的各个段就会分配到相应的虚拟地址。有的是0x08048000,有的是0x400000。

这里就有了一个问题,为什么链接器要将可执行文件虚拟地址分配到0x08048000,或者0x400000等其他地址?而不是从虚拟空间的0地址开始分配呢?

这涉及操作系统的进程虚拟地址空间的分配规则,在Linux下,ELF可执行文件默认从地址0x08048000开始分配。

符号地址的确定

在第一步的扫描和空间分配阶段,链接器按照前面介绍的空间分配方法进行分配,这时候输入文件中的各个段在链接后的虚拟地址就已经确定了。

当前面一步完成之后,链接器开始计算各个符号的虚拟地址。