这篇文章主要讲述链接中的静态链接。
1.对于链接器来说,在链接的过程主要解决的是将几个输入目标文件加工后和合并成一个输出文件这个文件是可执行文件。这时候就产生了问题,对于多个输入目标文件,编译器如何将他们各个段合并到输出文件。也就是说,输出文件的空间如何分配给各个输入文件。
2.链接其采用的是将相似的段合并,就是说将a.o 的 .text与b.o的.text合并等。这里注意.bss段,它在输出文件中并不占用文件空间,但是在装载的时候占用地址空间,所以连接器在合并各个段的时候也将.bss合并,并且分配虚拟的空间。链接器会为目标文件分配地址和空间,这里的地址与空间是两个含义,地址指的是输出的可执行文件中的空间,地址指的是装载后的虚拟地址中的虚拟地址空间。对于存在数据的段例如./text ./data来说,它们在文件中和虚拟地址中都要分配空间,而对于.bss段分配空间的意义只限于虚拟地址空间。
接下来就来讨论链接器究竟干了什么,一般链接器都采用两步链接(Two-pass Linking)的方法,
- 分配空间与地址
扫描所有输入文件获得各个段长,属性,位置,并将输入目标文件中的符号表中所有的符号收集起来村到全局符号表。
- 符号解析与重定位
读入文件的段,数据,重定位信息,并进行符号解析与重定位。调整代码中的地址,这一步是链接的核心。
我们写两个程序
//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;
}
我们使用ld a.o b.o -e main -o ab将a.o b.o 链接起来 -e表示 main函数作为入口函数。ld的默认入口为_start。
链接结束后可一通过objdump -h ab 各个段信息,与a.o b.o 各个段信息比较可以看出 ab中的VMA(Virtual Memory Adress)虚拟地址 和LMA(Load Memory Address)加载地址已经赋值了在a.o b.o中它们都是0。
当经过第一步扫描和空间分配阶段,链接器对输入文件各个段确定了虚拟地址。比如./text的其实地址为0x08048094 .data为0x08049108。这个时候链接器开始计算各个符号的虚拟地址,各个符号在段内的相对位置是个固定的,所以,"main","shared","swap"的地址已经确定了,只不过是链接器要为没一个符号加上一个偏移量,是他们得到正确的虚拟地址。
接下来就要进行第二步,符号的解析与重定位。
重定位。在分析静态链接是重定位之前来看一看在a.o中 是怎么使用两个外部符号的,shared和 swap,用objdump -
d a.o显示返汇编后的代码。
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: be 00 00 00 00 mov $0x0,%esi
18: 48 89 c7 mov %rax,%rdi
1b: b8 00 00 00 00 mov $0x0,%eax
20: e8 00 00 00 00 callq 25 <main+0x25>
25: c9 leaveq
26: c3 retq
(由于每个人的平台不一样,所以上面反汇编的结果不一定都一样)
上面标上颜色的是两个外部变量的汇编命令,编译器并不知道shared和swap的地址,因为他们定义在其他的目标文件中。所以编译器就暂时把地址0看作是shared,swap的地址。用objdump -d ab反汇编输出ab的代码可一看到现在的两个冲定位入口已经被修正到了正确的位置。
00000000004004b4 <main>:
4004b4: 55 push %rbp
4004b5: 48 89 e5 mov %rsp,%rbp
4004b8: 48 83 ec 10 sub $0x10,%rsp
4004bc: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
4004c3: 48 8d 45 fc lea -0x4(%rbp),%rax
4004c7: be 18 10 60 00 mov $0x601018,%esi
4004cc: 48 89 c7 mov %rax,%rdi
4004cf: b8 00 00 00 00 mov $0x0,%eax
4004d4: e8 03 00 00 00 callq 4004dc <swap>
4004d9: c9 leaveq
4004da: c3 retq
4004db: 90 nop
00000000004004dc <swap>:
4004dc: 55 push %rbp
4004dd: 48 89 e5 mov %rsp,%rbp
4004e0: 48 89 7d f8 mov %rdi,-0x8(%rbp)
.......................
接下来解释这两个地址是如何计算出来的。在此之前先介绍一下重定位表,重定位表的作用就是专门用来保存这些与重定位相关的信息。对于一个ELF文件来说他必须包含重定位表,用看来修改相应段的内容,对于没一个要冲定位的ELF段都有它相应的一个重定位表,一个重定位表是ELF文件的一个段,比如代码段.text如果有要被重定位的地方,就会有一个相应的叫".rel.text"段保存了代码段的重定位表,我们可 一通过objdump -r a.o来显示a.o中的重定位表。显示的效果是:
a.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000014 R_X86_64_32 shared
0000000000000021 R_X86_64_PC32 swap-0x0000000000000004
表中的OFFSET代表代码段中需要被调整的位置,可一查看刚才被反汇编后的a.o的代码的相应位置就是对应这两个外部符号。知道重定位表的作用后就来介绍符号解析。
每个目标文件都可能定义一些符号也可能引用到定义在其他文件的符号,重定位过程中也是符号解析的过程,每个重定位的入口都是对一个符号的引用,在确定一个符号的目标地址时链接器会去查找由所有输入目标的符号表组成的全局符号表,找到相应的符号后进行重定位。在确定目标地址的时候要进行指令修正。不同的处理器指令对地址的格式和方式都不一样。但是x86平台系的ELF文件的重定位如后所修正的指令寻址方式只有两种:
绝对近址寻址,相对近址寻址,
R_X86_64_32 1 绝对寻址修正 S + A
R_X86_64_PC32 2 相对寻址修正 S + A - P
A = 保存在被修正位置的值
P = 被修正的位置(相对与段开始的偏移)可以由 r_offset获得。
S = 符号的实际地址 由r_info高24位指定。
给你一堆这些公式符号可能很不好理解,接下来我们就通过上面所讲的例子来演示目标地址是如何得到的。
用 readelf -s ab查看链接后的ab中各个符号的地址
61 : 00000000004004b4 39 FUNC GLOBAL DEFAULT 13 main
62: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
63: 00000000004004dc 74 FUNC GLOBAL DEFAULT 13 swap
64: 0000000000400390 0 FUNC GLOBAL DEFAULT 11 _init
65: 0000000000601018 4 OBJECT GLOBAL DEFAULT 24 shared
可以清楚的看到 main swap shared的地址。上面打印出来的a.o的重定位表的信息中指出 share是采用的绝对寻址修正。所以采用的是 S + A的方式 ,
S是符号shared的实际地址为 0x601018
A是被修正的位置 为0x00000000
所以最后被修后的地址为 0x00601018
swap采用的是相对寻址修正方式所以采用的是 S + A - P
S 是swap当前的地址0x4004dc
A 是指被修正的位置值,在打印出重定位表的时候可以看到swap后面有一串数字-0x0000000000000004这就是 A
P 为被修正的位置,当链接成可执行文件时,这个值应该是被修正位置的虚拟地址,即0x4004b4(main的起始地址) + 0x21(相对于main的偏移)
所以,重定位入口修正后的地址为:0x4004dc + (-4) -(0x4004b4 + 0x21)= 0x000003 ,这条相对位移调用指令调用的地址是该指令下一条指令的起始地址加上这个算出来的偏移量。即,0x4004d9 + 0x000003 = 0x4004dc正好是swap函数的地址。
在回去看看上面反汇编出来的 ab代码正好和这两个计算结果吻合。
浙公网安备 33010602011771号