在通过编译和汇编后,就生成了目标文件,链接就是把这些目标文件加工后合并成一个输出文件的过程。
链接过程可以分为两步:
第一步 空间与地址分配。扫描所有的输入目标文件,获得它们每个各个段的长度、属性和位置,并且将输入目标文件中的符号表中所
有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的段长度,并且将它们合并
(相同的段互相合并,如.text和.text段合并、.data段和.data段合并),计算出输出文件中各个段合并后的长度和位置,并建立映射关系。
通过这一步,输入文件中的各个段在链接后的虚拟地址就确定了。我们可以通过objdump -h命令可以看到,链接前目标文件中的各段的地址
都为0,这是因为此时虚拟空间还没有分配,而链接后生成的可执行文件中的各段的地址已经分配。给可执行文件的各个段分配地址空间涉及
到操作系统的进程虚拟地址空间的分配规则,关于进程的虚拟地址分配的相关内容请参见
http://blog.csdn.net/vividonly/archive/2011/05/06/6399938.aspx一文。
分配并计算出可执行文件中各个段的虚拟地址后,链接器开始计算各个符号的虚拟地址。因为各个符号在段内的相对位置是固定的,所以这时候各个符号的地址已经是确定的了。举个例子,假设“a.o”中的“main”函数相对于“a.o”的“.text”段得偏移是X,并且经过链接合并各段后,“a.o”的“.text”段位于虚拟地址0x08048094(在Linux中,ELF可执行文件默认从地址0x08048000开始分配),那么"main"函数的地址应该是 0x08048094+X。通过这种方法,可以计算出所有符号的地址,并保存至全局符号表中。在后文中就将看到,这里计算出全局符号表中各个符号的地址是为了符号解析和重定位一步中计算指定修正值时使用的。
第二步 符号解析和重定位。使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析和重定
位、调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程。
通过objdump -d命令对目标文件进行反汇编后(能看到汇编代码),可以看到对应指令中外部引用的地址部分都是暂时用临时的假地
址来代替,真正的地址计算工作由富豪街西和重定位这一步来进行,也是符号解析和重定位的主要工作。通过前面的空间和地址分配可以得知,
链接器在完成地址和空间分配之后就已经可以确定所有符号的虚拟地址了,那么链接器就可以根据符号的地址对每个需要重定位的指令进行地
址修正。通过对可执行文件执行使用objdump -d命令进行反汇编,可以看到,相应的执行的地址都已经得到了修正。
那么链接器是怎么知道哪些指令是要被调整的呢?这些指令的哪些部分需要被调整?应该怎么调整?这都是通过重定位表来做的。通过
http://blog.csdn.net/vividonly/archive/2011/05/06/6399938.aspx一文,我们知道在ELF文件中,有一个重定位表用来保存与重定
位有关的信息。对于可重定位的ELF文件(目标文件)来说,它必须包含有重定位表。对于每个要重定位的ELF段都有一个对应的重定位表,
比如代码段“.text”如有要重定位的地方,那么会有一个相对应的“.rel.text”的段保存了了代码段的重定位表;如果数据段“.data”有要被重
定位的地方,就会有一个相对应的叫".rel.data"的重定位表。可以用objdump -r 命令来查看目标文件的重定位表,重定位表中列出了目标
文件中所有引用到外部符号的地方,亦即需要重定位的地方,每个需要重定位的地方叫做一个重定位入口。对于32位的Inter x86系列处理器
来说,重定位表的结构如下:
typedef struct{
Elf32_Addr r_offset;
Elf32_Word r_info;
}Elf32_Rel;
r_offset是重定位入口的偏移。对于目标文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于段起始的偏移;对于可
执行文件来说,这个值是该重定位入口所要修正的位置的第一个字节的虚拟地址。我们暂时只关心目标文件的情况。
r_info是重定位入口的类型和符号。这个成员的低8位表示重定位入口的类型,高24位表示重定位入口的符号在符号表中的下标。
重定位的过程中,每个重定位入口都是对一个符号的引用,那么当链接器需要对某个符号的引用进行重定位时,就要确定这个符号的目标地址,这时候链接器就会去查找所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。
下面进行最后的工作:修正指令。不同的处理器指令对于地址的格式和方式都不一样。比如对于32位Intel x86处理器来说,转移跳转指令(jmp指令)、子程序调用指令(call指令)和数据传送指令(mov指令)寻址方式千差万别。但是对于32位x86平台下的ELF文件的重定位入口所修正的指令寻址方式只有两种:绝对近址32位寻址、相对近址32位寻址。这两种重定位方式指令修正方式每个被修正的位置的长度都是32位,即4个字节。而且都是近址寻址,不用考虑Intel的段间远址寻址。唯一的区别就是绝对寻址和相对寻址。
前面提过,重定位的r_info成员的低8位表示重定位入口类型,如下所示:
宏定义 值 重定位修正方法
R_386_32 1 绝对寻址修正S+A
R_386_pc32 0 相对寻址修正S+A-P
其中A = 被修正位置的值
P = 被修正的位置(相对于段开始的偏移量或者虚拟地址),该值可通过r_offset计算得到
S = 符号的实际地址,即由r_info的高24位指定的符号的实际地址
假设我们有这样两个文件:
现在我们假设在将a.o和b.o连接成最终可执行文件后,main函数的虚拟地址为0x1000,swap函数的虚拟地址为0x2000,shared变量的虚拟地址为0x3000。那么链接器将如何修正a.o里面的两个重定位入口呢?
- 绝对寻址修正。通过objdump -r a.o可以看到,a.o中的第一个重定位入口是偏移为0x18的mov指令的修正,也就是对shared变量引用的修正。可以看到它的修正方式为R_386_32,即绝对地址修正。对于这个重定位入口,它修正的结果应该是S+A。S是符号shared的实际地址,即0x3000;A是被修正位置的值,通过objdump -d反汇编可以看到,此虚假地址值为0x00000000。故这个重定位入口修正后的地址为0x3000+0x00000000=0x3000。其实就是该符号的实际地址。
- 相对寻址修正。a.o中的第二个重定位入口是偏移为0x26的call指令的修正,也就是对swap函数的引用的修正。它的指令修正方式为R_386_PC32,即相对寻址修正。它修正后的结果是S+A-P。S是符号swap的实际地址,即0x2000。A是被修正位置的值,通过objdump -d看到此值为0xFFFFFFFC(-4的补码)。P为被修正的位置,当链接成可执行文件时,这个值应该是被修正位置的虚拟地址,即0x1000+0x27(0x27就是r_offset的值)。所以修正后的地址为0x2000+(-4)-(0x1000+0x27)=0xFD5。通过objdump -d可以看到,修正后的指令的下一条指令的地址为0x102b,而swap函数的地址为0x2000,易发现0x102b+0xFD5=0x2000,也就说这条被修正指令的下一条指令的地址加上修正后的地址值恰好等于swap函数的地址。可以得出以下结论:相对寻址修正后的地址为符号地址距离被修正位置的地址差。
注:链接命令ld用来将几个目标文件链接起来
ld a.o b.o -e main -o ab(-e main表示将main函数作为程序入口,-o ab表示输出文件名为ab)
经过了静态链接,便由若干个目标文件生成了一个可执行文件,接下来的就是可执行文件如何装载如内存的问题了。
本文深入探讨了链接过程的核心,包括目标文件转换为可执行文件的步骤,如空间与地址分配、符号解析与重定位等关键环节。详细解释了链接过程的两大部分:空间与地址分配以及符号解析与重定位,同时通过实例说明了如何计算符号地址及修正指令地址。最后介绍了链接器如何将静态链接转化为最终的可执行文件。

1206

被折叠的 条评论
为什么被折叠?



