以下面一个demo为例说明静态链接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a.c
extern int shared;
int main() {
int a = 100;
swap(&a, &shared);
}
// b.c
int shared = 1;
void swap(int *a, int *b) {
int* temp = b;
b = a;
a = b;
return;
}
将a.c和b.c经过编译后得到目标文件a.o和b.o,接下来要做的就是把a.o和b.o这两个目标文件链接在一起最终形成一个可以行的文件。
4.1 空间与地址分配
对于多个输入目标文件,链接器如何将他们的各个段合并到输出文件?或者说的更直白一点,输出文件中的空间如何分配给输入文件?
4.1.1 按序叠加
该方案并不是一个很好的选择,内存碎片太大。
4.1.2 相似段合并
将相似的段合并, 主要分为两个步骤
空间与地址分配: 扫描所有输入目标文件,获得段长度,属性和位置,将所有目标文件中的符号表中所有符号定义和符号引用收集,形成全局的符号表。
符号解析与重定位: 进行符号解析和重定位,调整代码中的地址等。
注意,链接前后程序中所使用的地址已经是诚心在进程中的虚拟地址,但是不是从0开始,Linxu下ELF可执行文件默认地址是从0x08048000开始分配。
4.1.3 符号地址的确定
- 这个时候输入文件中的各个段在链接后的虚拟地址已经确定了, 比如”text“段, ”.data“段等。
- 而后, 链接器开始计算各个符号的虚拟地址。因为符号在段内的相对位置是固定的。
4.2 符号解析与重定位
4.2.1 重定位
a.o 反汇编, objdump -d a.o
, 结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 为不确定地址的符号, a 变量为确定地址(相对于a.c源文件来说)。
- 编译器不知道 shared 和 swap 的地址, 即编译器暂时把
00000000
作为地址。
4.2.2 重定位表
链接器怎么知道哪些符号要重定位, 哪些符号不需要重定位的?
ELF文件中,需要重定位的段都有对应的重定位段( .data -> .data.relx, 下文都叫重定位表 )。
查看a.o的重定位信息, objdump -r a.o
1
2
3
4
5
6
7
8
9
10
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000025 R_X86_64_PC32 shared-0x0000000000000004
0000000000000032 R_X86_64_PLT32 swap-0x0000000000000004
000000000000004b R_X86_64_PLT32 __stack_chk_fail-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
两个重点:
- 重定位入口。
- 入口偏移。
4.2.3 符号解析
两层理解:
为什么要连接? 因为我们目标文件中所用到的符号被定义在其他目标文件中, 所以要将他们链接起来。 引入静态库, 经常会出现的符号未定义等,就是该原因。
1 2 3 4 5 6 7
// ld a.o ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0 a.o: In function `main': a.c:(.text+0x25): undefined reference to `shared' a.c:(.text+0x32): undefined reference to `swap' a.c:(.text+0x4b): undefined reference to `__stack_chk_fail'
当链接器需要对某个符号进行重定位时, 他要确定这个符号的目标地址,这个时候连接器就会去查找由所输入文件的符号表组成的全局符号表,找到相应的符号以后进行重定位。
4.3 COMMON块
由于强弱符号机制,允许同一个的定义在许多文件之中。若一个弱符号定义在了多个目标文件之中,而他们的类型又不同,怎么办? 连接器本身不支持符号段类型,即变量类型对于连接器来说是透明的。
所以,当我们定义多个符号类型不一致时, 编译器该如何处理?分为三种情况:
两个或两个以上强符号类型不一致 多个强符号定义本身就是违法的, 链接器会报多重定义错误。
一个强符号, 多个弱符号, 类型不一致。 输出强符号类型。如果弱符号所占空间大于强符号, ld连接器会警告。
两个或两个以上弱符号, 类型不一致。 输出空间较大的弱符号。
所以上述可以很好的解释, 为什么在目标文件中, 编译器不把未初始化的全局变量也当做一个未初始化的局部静态变量来处理, 为它在BSS段分配空间? 而是要标记成一个COMMON类型变量。
若编译器编译单元包含弱符号,此时不能确定该符号大小。
4.4 有关c++的那些内容
暂略。
4.5 静态库链接
链接过程:
GCC C语言编译器,生成临时汇编文件。
as 程序, 汇编生成目标文件。
GCC 调用collect2(ld链接器器的一个包装)链接。