《程序员的自我修养》学习笔记(四)————静态链接

        人们把每个源代码模块独立地编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是链接。链接的主要内容就是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确地链接。链接的过程主要包括地址与空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等步骤。符号决议有时也叫做符号绑定、名称绑定、名称决议、地址绑定等,“决议”更倾向于静态链接,而“绑定”则倾向于动态链接。
        当我们有两个目标文件时,如何将它们链接起来形成一个目标文件呢?这个过程中发生了什么?这个就是链接的核心内容:静态链接。

图1

我们以a.c和b.c为例分析该过程。

1. 空间与地址分配

        链接器就是把几个目标文件加工后合成一个输出文件。在这里,我们就需要把目标文件“a.o”和“b.o”,输出成可执行文件“ab”。从ELF文件格式中可以知道,可执行文件中的代码段和数据段都是由输入的目标文件中合并来的。那么多个输入目标文件,链接器如何把他们各自的段合并到输出文件,也就是输出文件中的空间如何分配给输入文件。

1.1 按序叠加       

        按序叠加,就是直接将各个目标文件依次合并,但是这样会造成在有多个文件的输入下,输出文件将有很多零散的段。而且一般的硬件都有字节对齐,这样很容易造成内存碎片,浪费。

1.2 相似段合并

       相似段合并,就是将相同性质的段合并到一起,比如所有的输入文件的“.text”合并到输出文件的“.text”段等。
“链接器为目标文件分配地址和空间”这里的“地址和空间”有两个含义:(1)可执行文件中的空间;(2)在装载后的虚拟地址中的虚拟地址空间。
        现在的链接器空间分配主要是第二种,使用这种链接器一般采用两步链接:
第一步:空间与地址分配
        扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
第二步:符号解析与重定位
        使用上一步收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。第二步是链接过程的核心,特别是重定位过程。

图2

        其中VMA表示Virtual Memory Address,即虚拟地址,LMA表示Load Memory Address,即加载地址。
        链接前后的程序中所使用的地址已经是程序在进程中的虚拟地址,即我们关心上面各个段中的VMA和Size,忽略File off。可以看到,目标文件中的所有段的VMA都是0,因为虚拟空间还没有被分配,所以它们默认为0。等链接以后,可执行文件“ab”中的各个段都被分配到了相应的虚拟地址。

1.3 符号的确定

图3

         从表中的Value(地址)可以看出main函数和swap函数位于.text段中,shared位于.data段中。所以,“a.o”、“b.o”的各个段已经合并在“ab”的相应的段中了。

2. 符号解析与重定位

2.1 重定位

          在完成空间和地址的分配步骤以后,链接器就进入了符号解析与重定位的步骤,这也是静态链接的核心内容。在分析符号解析和重定位之前,首先看看“a.o”里面是怎样使用这两个外部符号的,也就是说我们在“a.c”的源代码里面使用了“shared”变量和“swap”函数,那么编译器在将“a.c”编译成指令时,它如何访问“shared”变量?如何调用“swap”函数?

图4

        我已经用红框标出了两个引用“shared”和“swap”的位置。由于编译器不知道他们的地址,所以使用“0x00000000”代替着,把真正的地址计算工作留给链接器。查看“ab”的反汇编如下图所示;

图5

         经过修正以后,“shared”和“swap”的地址分别为0x006001b8和0x00000003。关于“shared”变量的地址,我们可以从图3符号表中可以看到,确实是0x006001b8。“callq”指令后面跟着的是指令的下一条指令的偏移量,即0x40010d+0x00000003=0x400110。

2.2 重定位表

       链接器是怎么知道哪些指令是要被调整的呢?在ELF文件中,有一个重定位表(Relocution Table)的结构专门来保存这些与重定位相关的信息。对于可重定位的ELF文件来说,它必须包含有重定位表,用来描述如何修改相应的段里的内容。对于每个要重定位的ELF段都有一个相应的重定位表,而一个重定位表往往就是ELF文件中的一个段,所以其实重定位表也叫重定位段,这里我们统一称为重定位表。代码段“.text”如有要重定位的地方,那么就会有一个相对应叫“.rel.text”的段保存了代码段的重定位表。

图6

        查看“a.o”里面要重定位的地方,即“a.o”所有引用到外部符号的地址。每个要被重定位的地方叫一个重定位入口,“a.o”有两个重定位入口。从图4中可以看出这两个偏移就是代码段中“mov”指令和“callq”指令的地址位置。
        重定位表的结构也很简单,它是一个结构体的数组,每个数组元素对应一个重定位入口。

图7

2.3 符号解析

图8

        我们查看“a.o”的符号表,如图8所示。“Global”类型的符号,除了“main”函数是定义在代码段中,其他连个个 “shared” 和“swap”都是“UND”,即“undefined”未定义类型,这种未定义的符号就是因为该目标文件中有关于它们的重定位项。所以链接器扫描完所有的输入目标文件后,所有这些未定义的符号都应该能够在全局符号表中找到,否则链接器就报符号未定义错误,如图9所示。

图9

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章