计算机系统基础--第四章(程序的链接)

什么是程序的链接?

将gcc生成的若干个.0文件为后缀的,vc输出为.obj为后缀的可重定位目标文件组合起来,生成一个可执行目标文件

程序链接的好处:

1、模块化(可分成不同模块全部编译成.o文件后链接即可运行。而不仅仅是只有一个.c文件,对于大工程来说清晰划分)
2、效率高(由于可以使多个文件同时编写,多个模块同时修改,因此这样提升了效率)

在链接先进行编译和汇编,在第三章聊过

可执行文件与.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
28
29
30
31
32
33
34
35
36
37
// test.c
int main(int i,int j){
int x = i+j;
return x;
}
//使用objdump -d test.o反汇编出来的.o文件
// test.o
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d ec mov %edi,-0x14(%rbp)
7: 89 75 e8 mov %esi,-0x18(%rbp)
a: 8b 55 ec mov -0x14(%rbp),%edx
d: 8b 45 e8 mov -0x18(%rbp),%eax
10: 01 d0 add %edx,%eax
12: 89 45 fc mov %eax,-0x4(%rbp)
15: 8b 45 fc mov -0x4(%rbp),%eax
18: 5d pop %rbp
19: c3 retq
//每个.o文件开头都是以0开始,还没有被链接成位可执行文件,在其内部为虚拟地址
//使用objdump -d test反汇编出来的可执行文件
//test
00000000004004d6 <main>:
4004d6: 55 push %rbp
4004d7: 48 89 e5 mov %rsp,%rbp
4004da: 89 7d ec mov %edi,-0x14(%rbp)
4004dd: 89 75 e8 mov %esi,-0x18(%rbp)
4004e0: 8b 55 ec mov -0x14(%rbp),%edx
4004e3: 8b 45 e8 mov -0x18(%rbp),%eax
4004e6: 01 d0 add %edx,%eax
4004e8: 89 45 fc mov %eax,-0x4(%rbp)
4004eb: 8b 45 fc mov -0x4(%rbp),%eax
4004ee: 5d pop %rbp
4004ef: c3 retq
// 由于我的机子是8g内存,因此开头以400...开头
// test.o与test相比,test经过链接后,将虚拟地址转化成为逻辑地址

目标文件格式

ELF格式
每个可执行文件都有一个ELF头,里面包括着可执行文件的信息。

ELF头

elfImage
夹在ELF头和节头部表之间的都是节。一个典型的ELF可重定位目标文件包含下面几个节:
.text:已编译程序的机器代码。
.rodata:只读数据,比如printf语句中的格式串和开关(switch)语句的跳转表。
.data:已初始化的全局C变量。局部C变量在运行时被保存在栈中,既不出现在.data中,也不出现在.bss节中。
.bss:未初始化的全局C变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分初始化和未初始化变量是为了空间效率在:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。
.symtab:一个符号表(symbol table),它存放在程序中被定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的表目。
.rel.text:当链接噐把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非使用者显式地指示链接器包含这些信息。
.rel.data:被模块定义或引用的任何全局变量的信息。一般而言,任何已初始化全局变量的初始值是全局变量或者外部定义函数的地址都需要被修改。
.debug:一个调试符号表,其有些表目是程序中定义的局部变量和类型定义,有些表目是程序中定义和引用的全局变量,有些是原始的C源文件。只有以-g选项调用编译驱动程序时,才会得到这张表。
.line:原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译驱动程序时,才会得到这张表。
.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。
旁注:为什么未初始化的数据称为.bss?
用术语.bss来表示未初始化的数据是很普遍的。它起始于IBM 704汇编语言(大约在1957年)中”块存储开始(Block Storage Start)“指令的首字母缩写,并沿用至今。一个记住区分.data和.bss节的简单方法是把“bss”看成是“更好地节省空间(Better Save Space)!“的缩写。

符号表和符号解析

符号表的类型

1、在模块m中定义冰杯其他模块引用的全局符号
2、在其他模块定义并且被m引用的外部符号
3、在模块m中定义并在m中引用的本地符号

使用readelf -s prog.o查看符号表
1
2
3
4
5
6
7
8
9
10
11
12
13
readelf -s main.o
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
8: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 buf
9: 0000000000000000 16 FUNC GLOBAL DEFAULT 1 main
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
readelf -s swap.o
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000000 8 OBJECT LOCAL DEFAULT 5 bufp1
9: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 bufp0
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND buf
11: 0000000000000000 60 FUNC GLOBAL DEFAULT 1 swap

GLOBAL位全局变量,LOCAL为局部变量,swap为函数,Ndx位本地变量,UND为undefined

符号解析
全局符号的强弱特性

定义:函数名和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
例如上面的,main,buf,swap,bufp0是 强符号,bufp1位本地符号,本地符号无强弱之分。

多重定义符号处理:
规则1:强符号不能多次定义,也即强符号只能被定义一次,否则链接错误
规则2:若一个符号被说明为一次强符号定义和多个弱符号定义,则按强符号为准
规则3:若有多个弱符号定义,则任选其中一个

强弱符号链接错误解决方案:

1、尽量避免使用全局符号
2、把全局符号定义位static,这样就没有强弱之分
3、尽量要给全局变量赋初值使其变成强符号
4、外部全局变量尽量使用extern

重定位

规则:
函数调用采用相对重定位
即使用R_386_PC32:ADDR(r_sym)-((ADDR(.text)+ r_offset) - init)
全局变量采用绝对重定位
即使用R_386_32把32位的地址值直接代替

重定位的工作

1、节和定义符号的重定位
2、引用符号的重定位

链接

1、静态链接:将用户程序中使用的库文件完整拷贝形成一个完整的可执行文件
优点:可随时执行文件,可执行文件不会因为库文件丢失而无法执行
缺点:导致相同库文件多个备份
2、动态链接:根据代码、数据、重定位和符号表信息,能在执行目标文件是装入或运行被动态的装入内存并自动链接
优点:减少库文件的多个备份
缺点:缺少库文件无法运行

小结:

至此,第四章也完成了。这一章主要讲的是生成可执行文件的过程,由最初始的编辑代码,然后将其预编译,编译,汇编,链接,生成可执行文件。这个过程是不仅由我们看到的小黑框那么简单,这段时间还经历着将代码转成汇编,将所定义的全局变量以及函数名称建表,把各个符号通过重定位形成一个最终没有缺失的可执行文件。第四章让我很清晰的清楚可执行文件的生成,也把当初的黑匣子打开,让我更深刻的理解其工作原理。