链接
Object file
在Linux系统中主要有三种object file,它们使用Excutable and Linkable Format (ELF):
- relocatable object file 可重定位的目标文件
- executable object file 可运行的目标文件
- shared object file 共享目标文件
链接
链接其实就是由relocatablle object file和shared object file生成executable object file的过程,executable object file可以加载到内存中执行。
考虑一下这个过程,在relocatable object file中的变量类型:
- 在本文件中定义的全局变量(函数)
- 在本文件中定义的静态变量(函数)
- 在本文件中定义的非静态局部变量
- 在其他文件中定义的全局变量(函数)
其中前面三种变量都可以在当前文件中找到定义,而如何解析最后一种变量就是链接过程需要做的工作。
ELF sections
ELF包含了几个关键的sections,理解这些sections的作用会对理解链接过程起到很大的作用。
Section | 描述 |
---|---|
.text | 程序代码段 |
.data | 初始化的全局变量 |
.bss | 未初始化的全局变量 |
.symtab | 符号表 |
.debug | 变量调试符号表 |
.rodata | 只读数据,如printf中的format strings,switch的jump tables |
... | ... |
看一个具体的例子,修改于CSAPP中提供的例子:
// main.c
void swap(void);
int buf[2] = {1, 2};
int main(void)
{
static si = 0;
++si;
char *str = "swap():";
swap();
printf("%s success\n", str);
return 0;
}
// swap.c
extern int buf[];
int *bufp0 = &buf[0];
int *bufp1;
void swap()
{
static int si;
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
接下来完成预处理、编译、汇编和链接四个阶段,可以得到一个可执行文件,具体的过程可以参见man gcc
,如-E选项会使得gcc在preprocessing stage后停止。
在汇编阶段之后会获得main.o和swap.o,这两个object file是relocatable的,使用readelf来查看它们的符号表:
Machine: X86-64 ubuntu GNU readelf: 2.24.90.20141014
# readelf -a main.o
Symbol table '.symtab' contains 15 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 si.2203
7: 0000000000000000 0 SECTION LOCAL DEFAULT 6
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 5
10: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 buf
11: 0000000000000000 16 FUNC GLOBAL DEFAULT 1 main
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __stack_chk_fail
# nm main.o
0000000000000000 D buf
0000000000000000 T main
0000000000000000 b si.2203
U __stack_chk_fail
U swap
# objdump -s main.o
...
Contents of section .rodata:
0000 25732073 75636365 73730a00 %s success..
...
main.o的符号表中包含了buf,main和swap等五个符号,buf处在初始化的数据段中(.data),main处在代码段中(.text),swap符号没有定义(undefined),本地静态变量si处于未初始化的数据段中(.bss),注意在符号表中它加上了一个后缀,这样如果在其他的object file中定义了相同名字的本地静态变量si,它们之间不会产生冲突。printf中的format string存在了rodata段中。
# readelf -a swap.o
Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS swap.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5
5: 0000000000000000 4 OBJECT LOCAL DEFAULT 5 si.2202
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 bufp0
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND buf
11: 0000000000000008 8 OBJECT GLOBAL DEFAULT COM bufp1
12: 0000000000000000 59 FUNC GLOBAL DEFAULT 1 swap
# nm swap.o
U buf
0000000000000000 D bufp0
0000000000000008 C bufp1
0000000000000000 b si.2202
0000000000000000 T swap
swap.o的符号表中包含了四个符号。bufp1是common symbol,它存在.bss中,注意bufp0和bufp1的区别,前者是已初始化的全局变量,它是一个strong符号,而bufp1是一个weak符号。
strong符号(初始化的全局变量和函数)和weak符号(未初始化的全局变量)是由编译器确定的,在全局符号的解析过程中,当遇到同名的符号时:
- 不允许出现多个同名的强符号
- 当出现一个强符号和多个弱符号时,选择强符号
- 当出现多个弱符号时,任意选择一个弱符号
链接过程实际上是实现了符号的解析和重定位过程。
gcc -o p main.o swap.o
变量存储的位置及变量作用域的实现
- 全局变量:存储在bss或data,所有文件都可以访问
- 本地静态变量:存储在bss或data,当前文件可访问
- 静态分配的局部变量:存储在栈上,只有当前函数可以访问
- 动态分配的局部变量:存储在堆上,如果没有被释放,而且保存了变量的地址,所有文件都可以访问
静态链接和动态链接
为什么需要静态链接库?
考虑如何为应用程序提供C库的支持:一种是编译器自身能理解所有c库中的符号,执行相应的链接操作;另一种是将所有C库编译成一个relocatable oject file。但这样存在一些明显的缺点,如果要对修改C库的接口,第一种选择就需要改变编译器的实现,第二种选择需要重新编译生成新的relocatable object file。
人的智慧是无穷的,更好的方法就是将一个个的库函数分别编译成一个个的relocatable object file,然后将它们打包成一个archive,称为静态链接库。这样无论是修改还是更新C库的接口带来的影响都比较小。
在实际的编译过程中,编译器纪录下当前未被解析的符号定义,然后如果在静态链接库中找到相应的符号,就将相应的relocatable object file包含进来一起链接。
为什么需要动态链接库?
考虑一下这个画面,小明一个学期写了100个C程序,每个程序中都使用了printf()函数,如果使用静态链接库,同时执行这100个程序,内存中就会有printf.o(包含printf()的object file)的100个拷贝,对于内存这种能省即省的资源,这绝对是一种浪费!
天下大势,总有更牛逼的人出现。然后动态链接库就被设计出来了,shared object file在内存中只保留一个副本,链接器在链接时只是拷贝一些符号信息,实际的链接过程推迟到了运行时。