理解静态链接
静态链接 (static linking) 是指将多个目标文件链接成一个可执行文件的过程:
1
2
3
4
gcc -c a.c b.c
gcc -o ab a.o b.o
# or
ld -e main -o ab a.o b.o
由于
ld
没有添加__start
、链接标准库等处理,这里需要指定入口。这里也不能使用printf
等库函数。collect2
包装了ld
链接器,进行了一些额外处理,是gcc/g++
实际使用的工具。
可重定位文件/目标文件
讲解链接过程之前,先介绍一下目标文件 (object file) 或者说可重定位文件 (relocatable file)。考虑如下代码的反汇编:
1
2
3
4
5
6
7
8
extern int var;
int myabs(int x);
int main() {
int a = myabs(var);
return 0;
}
objdump -drwC
反汇编结果:
1
2
3
4
5
6
7
8
9
10
11
12
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 10 sub $0x10,%rsp
c: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 12 <main+0x12> e: R_X86_64_PC32 var-0x4
12: 89 c7 mov %eax,%edi
14: e8 00 00 00 00 call 19 <main+0x19> 15: R_X86_64_PLT32 myabs-0x4
19: 89 45 fc mov %eax,-0x4(%rbp)
1c: b8 00 00 00 00 mov $0x0,%eax
21: c9 leave
22: c3 ret
有几个特点:
- 目标文件的代码段地址是从 0 开始的。使用
objdump -h
可以发现,不仅是代码段,所有段的虚拟内存地址都是从 0 开始的。 - 使用变量
var
和函数myabs
的地方,全部用 0 替代。
链接时重定位
静态链接采用相似段合并,即将多个目标文件中的 .text
段合并为一个大的 .text
。使用这种方法的链接器一般都采用两步链接(Two-pass Linking)方法:
- Pass 1: 读取 section 大小,计算最终的内存布局。同时,读取所有符号 symbol,在内存中创建完整的符号表(全局符号表,global symbol table)。
- Pass 2: 读取 section 和重定位信息,进行符号解析,调整代码中的地址,将新文件写出。
其中,第二步就是链接时重定位 (link time relocation) 每个需要重定位的段都有一个对应的重定位表 (relocation table),例如,这里的 .text
段对应的重定位表是 .rela.text
。我们可以通过 objdump -r
查看重定位表:
1
2
3
4
5
6
7
$ objdump -r a.o
test2.o: 文件格式 elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
000000000000000e R_X86_64_PC32 var-0x0000000000000004
0000000000000015 R_X86_64_PLT32 myabs-0x0000000000000004
其中,OFFSET
代表重定位入口 (relocation entry) 在所属段内的偏移,这里可以和前面的反汇编相印证。TYPE
代表重定位类型,将决定重定位的具体方式。
链接时,链接器每遇到一个重定位入口,首先要在全局符号表中查找符号,找到后,一般会利用如下信息,修改重定位入口的指令:
- 入口原指令中的值。比如
mov 0x0(%rip),%eax
中的00 00 00
。 - 链接后,入口的虚拟地址。它等于段的虚拟地址 + 入口在段内的偏移。(入口地址和入口指令地址有略微的差值)
- 链接后,符号定义的虚拟地址。
还要注意一点的的是,在 gcc 10.1 之后,除非显式定义了弱符号,否则任何定义在链接时都不能有两份。
链接顺序
在链接内命令中,源文件、目标文件、静态库文件出现的顺序也有影响。以下是我自己的一些总结,可能有误:
- 源文件先被编译为目标文件,所以这里和其他目标文件一起考虑。
- 链接顺序是从左到右,无论是目标文件还是库文件。具体规则是:
- 普通目标文件直接链接:如果它提供了需要重定位的符号的定义,则进行重定位。
- 静态库内目标文件按需链接:在静态库搜索当前未重定位的符号,搜索顺序为打包顺序。搜索到定义之后,将库内定义所在的目标文件链接,并进行重定位。
- 每链接一个目标文件时,如果遇到了需要重定位的符号,分两种情况考虑:
- 如果是普通目标文件,则先在已链接内容中查找符号,找到则进行重定位。
- 如果是静态库内的目标文件,则什么也不做。
可以发现,按照如上规则,假如静态库 A 依赖于静态库 B,那么链接时,应该将 A 放在 B 的前面。否则,A 中需要重定位的符号将无法被重定位。有一种特殊情况是,静态库 A 和 B 相互有依赖,此时可以用 -Wl,--start-group -lA -lB -Wl,--end-group
选项来解决。
设计规范
我们注意到在静态库中,一个目标文件通常只包含一个函数,比如 printf.o
只包含 printf
函数。这样做的好处是,链接 printf.o
时不会将其他不需要的函数也链接进来。它还能避免潜在的多重定义问题。
装载时重定位
如果我们希望可执行文件可以被装载到虚拟地址空间中的任意地址,就需要装载时重定位技术,相关内容将在动态链接一文中讲解。
Comments