案例探索
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;
}
gcc -c a.c b.c
我们得到了a.o ,b.o 文件。
b.c 总共定义了两个全局符号,变量shared ,函数swap a.c 总共定义了一个全局符号,函数main - 模块
a.c 引用了b.c 里面的swap 和shared
我们需要把a.o 和b.o 两个目标文件链接在一起并最终形成一个可执行文件
空间与地址分配
在有很多输入文件情况下,输出文件将会有很多零散的段。这会浪费空间,毕竟这设计到需要空间对齐的要求。那么必然会有空余出来的,用于凑整的空间被浪费掉。
可执行文件中的代码段和数据段都是由输入文件中合并而来的,那么链接器是如何将它们的各个段合并到输出文件中的?
一个更实际的方法是将相同性质的段合并到一起。链接器将相似的段合并到一起,比如将输入文件所有的.text 段合并到输出文件的.text 段。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X4LHYQ7h-1650725766092)(https://syz-picture.oss-cn-shenzhen.aliyuncs.com/D:%5CPrograme%20Files(x86)]%5CPicGoimage-20220131160006444.png)
之前提到,.bss 段在目标文件和可执行文件中并不占用文件空间,但是它在装载时占用地址空间(进程的虚拟地址)。所以链接器在合并各个段时,也将.bss 段合并,并分配虚拟空间。
链接器为目标文件分配地址和空间,这里地址和空间有两个含义
- 在输出中的可执行文件的空间
- 在装载后的虚拟地址中的虚拟地址空间(主要关注这个)
一般链接过程分两步
- 第一步:空间与地址分配
- 扫描所有输入目标文件,获得各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步,链接器可以获得所有输入目标文件中的段长度,并将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系
- 第二步:符号解析与重定位(核心)
- 使用上面第一步中收集到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符号解析与重定位,调整代码中的地址等。
使用objdump 查看链接前后地址的分配(VMA )
objdump -h a.o
Idx Name Size VMA LMA File off Algn
0 .text 00000051 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000091 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000091 2**0
objdump -h b.o
Idx Name Size VMA LMA File off Algn
0 .text 0000004b 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 0000008c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000090 2**0
ALLOC
我们发现在未进行链接时,a.o 和b.o 文件的虚拟地址空间都为0 ,因为虚拟地址空间还未被分配,所以都默认为0。而且分配的时候,默认是从0x08048000 开始分配的,给0 也代表这个值不可用。
objdump -h ab
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000238 0000000000000238 00000238 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 0000000000000254 0000000000000254 00000254 2**2
...
13 .text 00000222 0000000000000560 0000000000000560 00000560 2**4
链接后,VMA 开始有值,说明各个段都被分配了相应的虚拟地址空间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uT01S4vN-1650725766093)(https://syz-picture.oss-cn-shenzhen.aliyuncs.com/D:%5CPrograme%20Files(x86)]%5CPicGoimage-20220131161551546.png)
符号地址的确定
在链接器第一步扫描和空间分配阶段,输入文件中的各个段在链接后的虚拟地址就已经确定了。比如.text 段的起始地址为0x8048094
当前一步完成后,链接器开始计算各个符号的虚拟地址。比如a.o 的main 函数相对于a.o 的代码段的偏移为X ,则经链接合并以后,a.o 的代码段位于虚拟地址0x8048094 ,那么main 的地址为0x8048094+ X 。
符号解析与重定位
完成空间和地址的分配后,链接器就进入了符号解析与重定位步骤。
编译器在将a.c 编译成指令时,它是如何访问shared 变量如何调用swap 函数的?
objdump -d a.o
a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
f: 00 00
11: 48 89 45 f8 mov %rax,-0x8(%rbp)
15: 31 c0 xor %eax,%eax
17: c7 45 f4 64 00 00 00 movl $0x64,-0xc(%rbp)
1e: 48 8d 45 f4 lea -0xc(%rbp),%rax
22: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 29 <main+0x29>
29: 48 89 c7 mov %rax,%rdi
2c: b8 00 00 00 00 mov $0x0,%eax
31: e8 00 00 00 00 callq 36 <main+0x36>
36: b8 00 00 00 00 mov $0x0,%eax
3b: 48 8b 55 f8 mov -0x8(%rbp),%rdx
3f: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
46: 00 00
48: 74 05 je 4f <main+0x4f>
4a: e8 00 00 00 00 callq 4f <main+0x4f>
4f: c9 leaveq
50: c3 retq
当源代码a.c 在被编译成目标文件是,编译器并不知道shared 和swap 的地址,因为他们定义在b.c 中,所以编译器暂时将他们的地址看作0
而经历链接后,地址已经被确认
重定位表
那么链接器是怎么知道哪些指令要被调整呢?
之前在ELF 文件中,有一个叫做重定位表的存在,这个结构是专门保存这些与重定位相关的信息。每个要被重定位的ELF 段都有一个对应的重定位表,而一个重定位表往往就是ELF 文件中的一个段。
使用objdump 命令查看重定位表
objdump -r a.o
a.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000025 R_X86_64_PC32 shared-0x0000000000000004
0000000000000032 R_X86_64_PLT32 swap-0x0000000000000004
- 这个命令可以查看所有要被重定位的地方,每个被重定位的地方叫一个重定位入口。
RELOCATION RECORDS FOR [.text]: 代表.text 段的重定位表- 我们看到有两个重定位入口,重定位入口的偏移表示该入口在要被重定位的段中的位置。(正好对应上了反汇编指令中的
mov 指令和call 指令)
符号解析
重定位的过程中,每个重定位的入口就是对一个符号的引用,那么当链接器要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址。这时候链接器就会去查找由所有目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。
readelf -s a.o
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
readelf -s ab
62: 00000000000006bb 75 FUNC GLOBAL DEFAULT 14 swap
65: 0000000000201010 4 OBJECT GLOBAL DEFAULT 23 shared
可以看到,经过链接过后,UND 改变,说明在全局符号表中找到了。
COMMON块
如果一个弱符号定义存在于多个文件中,且它们的类型又不同,那么链接器如何选择
链接器遇到的三种情况
- 两个或两个以上强符号类型不一致(无需处理,链接器会报符号多重定义错误)
- 有一个强符号,其他都是弱符号,出现类型不一致
- 两个或两个以上弱符号类型不一致
现在编译器和链接器都支持一种叫做COMMON 块的机制。在早期Fortran 没有动态分配空间的机制的时候,程序员必须实现声明他所需要的临时使用空间的大小,Fortran 把这种空间叫COMMON 块,当不同的目标文件需要的COMMON 块大小不一致时,选择最大的那一块。
而现代链接器处理不同类型的弱符号就是采用COMMON 块的处理方法,选择占据空间最大的那一个
|