2.3 静态链接
2.3.1 地址空间分配
通过上一节对ELF文件格式的介绍,可以引出一个很自然的问题,两个或者多个不同的目标文件是如何组成一个可执行文件的呢?这就需要进行链接(linking)。链接由链接器(linker)完成,根据发生的时间不同,可分为编译时链接(compile time)、加载时链接(load time)和运行时链接(run time)。
将上一节的示例稍作改动后拆分成两个文件main.c和func.c,在用GCC静态编译时通过参数“-save-temps”把中间产物也打印出来,整个流程和2.1节中讲的差不多。
在将main.o和func.o这两个目标文件链接成一个可执行文件时,最简单的方法是按序叠加,如图2-5左半部所示。这种方案的弊端是,如果参与链接的目标文件过多,那么输出的可执行文件会非常零散。而段的装载地址和空间以页为单位对齐,不足一页的代码节或数据节也要占用一页,这样就造成了内存空间的浪费。
另一种方案是相似节合并,将不同目标文件相同属性的节合并为一个节,如将main.o与func.o的.text节合并为新的.text节,将main.o与func.o中的.data节合并为新的.data节,如图2-5右半部所示。这种方案被当前的链接器所采用,首先对各个节的长度、属性和偏移进行分析,然后将输入目标文件中符号表的符号定义与符号引用统一生成全局符号表,最后读取输入文件的各类信息对符号进行解析、重定位等操作。相似节的合并就发生在重定位时。完成后,程序中的每条指令和全局变量就都有唯一的运行时内存地址了。
图2-5 两种不同的链接方案
2.3.2 静态链接的详细过程
为了构造可执行文件,链接器必须完成两个重要工作:符号解析(symbol resolution)和重定位(relocation)。其中,符号解析是将每个符号(函数、全局变量、静态变量)的引用与其定义进行关联。重定位则是将每个符号的定义与一个内存地址进行关联,然后修改这些符号的引用,使其指向这个内存地址。
下面我们对比一下静态链接可执行文件func.ELF和中间产物main.o的区别。使用objdump可以查看文件各个节的详细信息,这里我们重点关注.text、.data和.bss节。
其中,VMA(Virtual Memory Address)是虚拟地址,LMA(Load Memory Address)是加载地址,一般情况下两者是相同的。可以看到,尚未进行链接的目标文件main.o的VMA都是0。而在链接完成后的func.ELF中,相似节被合并,且完成了虚拟地址的分配。
使用objdump查看main.o的反汇编代码,参数“-mi386:intel”表示以intel格式输出。
可以看到main()函数的地址从0开始。其中,对func()函数的调用在偏移0x20处,0xe8是CALL指令的操作码,后四个字节是被调用函数相对于调用指令的下一条指令的偏移量。此时符号还没有重定位,相对偏移为0x00000000,在这个目标文件中,CALL指令下一条MOV指令的地址为0x20,因此CALL指令调用的地址为0x20+(-0)=0x20,这只是一个临时地址,编译器其实并不知道位于另一个文件中的func()函数的实际地址,于是就把地址计算的工作交给链接器;链接器将根据上一步的结果对重定位符号的地址进行修正。同理,偏移0x13处是对shared的取值指令,但此时并不知道它的值,就暂时以0x00000000代替。
接下来,查看链接完成后func.ELF中的符号地址。
可以看到,调用func()函数的指令CALL位于0x4009c9,其下一条指令MOV位于0x4009ce,因此相对于MOV指令偏移量为0x07的地址为0x4009ce+0x07=0x4009d5,刚好就是func()函数的地址。同时,0x4009c1处也已经改成了shared的地址0x6ca090。
可重定位文件中最重要的就是要包含重定位表,用于告诉链接器如何修改节的内容。每一个重定位表对应一个需要被重定位的节,例如名为.rel.text的节用于保存.text节的重定位表。.rel.text包含两个重定位入口,shared的类型R_X86_64_32用于绝对寻址,CPU将直接使用在指令中编码的32位值作为有效地址。func的类型R_X86_64_PC32用于相对寻址,CPU将指令中编码的32位值加上PC(下一条指令地址)的值得到有效地址。需要注意的是,func-0x0000000000000004中的-0x4是r_addend域的值,是对偏移的调整,如下所示。
2.3.3 静态链接库
后缀名为.a的文件是静态链接库文件,如常见的libc.a。一个静态链接库可以视为一组目标文件经过压缩打包后形成的集合。执行各种编译任务时,需要许多不同的目标文件,比如输入输出有printf.o、scanf.o,内存管理有malloc.o等。为了方便管理,人们使用ar工具将这些目标文件进行了压缩、编号和索引,就形成了libc.a。