链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载到存储器并执行。
链接于编译时,也就是源代码被翻译成机器代码时;
链接于加载时,也就是在程序被加载器加载到存储器执行时;
链接于运行时,由应用程序来执行。
传统静态链接、加载时的共享库动态链接、运行时的共享库动态链接;
在早期链接是手动执行的;
在现代系统中,链接是由链接器(linker)的程序自动执行的;
链接由链接器默默处理;
链接在软件开发中扮演非常重要的角色;它使得分离编译成为可能。
这样我们不用将一个大型应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块。
可以独立地修改和编译这些模块。
当我们改变这些模块中的一个时,只需要简单地重新编译它。
学习链接有什么好处?
1、理解链接器可以帮助你构造大型程序:
经常遇到缺少模块、缺少库或者不兼容的库版本引起的链接器错误;
必须理解如何解析引用、什么是库、链接器是如何使用库来解析引用;
2、理解链接器可以帮助避免一些危险的编程错误:
Unix链接器解析符号引用时所做的决定回不动声色地影响你程序的正确性;
错误地定义多个全局变量的程序将通过链接器,而不产生任何警告信息;
3、理解链接帮助你理解语言的作用域规则如何实现:
全局和局部变量之间的区别是什么?
当定义一个具有static属性的变量或者函数时,到底实际意味着什么?
4、理解链接帮助你理解其他重要的系统概念:
链接器产生的可执行目标文件在重要的系统功能中扮演重要角色;
比如加载和运行程序、虚拟存储器、分页和存储器映射;
5、理解链接让你能够利用共享库:
共享库和动态链接在现代操作系统中扮演越来越重要的角色,链接成为一个复杂的过程;
===================================================
一、编译器驱动程序
大多数编译系统提供编译驱动程序(complier driver),它代表用户在需要时调用语言预处理器、编译器、汇编器、链接器;
以两个示例程序为例:
1 // main.c 2 void swap(); 3 4 int buf[2] = {1, 2}; 5 6 int main() 7 { 8 swap(); 9 return 0; 10 }
1 //swap.c 2 3 extern int buf[]; 4 5 int *bufp0 = &buf[0]; 6 int *bufp1; 7 8 void swap() 9 { 10 int temp; 11 12 bufp1 = &buf[1]; 13 temp = *bufp0; 14 *bufp0 = *bufp1; 15 *bufp1 = temp; 16 }
例如要用GNU编译系统构造示例程序,就要在shell中输入一下命令:(可以用-v来查看详细步骤)
unix>gcc -02 -g -o p main.c swap.c
驱动程序首先运行C预处理器(cpp),它将C源程序main.c翻译成ASCII码的中间文件main.i;
即: cpp [other arguments] main.c /tmp/main.i
接下来,驱动程序运行C编译器(cc1),它将main.i翻译成一个ASCII汇编语言文件main.s;
即:cc1 /tmp/main.i main.c -o2 [other arguments] -o /tmp/main.s
然后,驱动程序运行汇编器(as),它将main.s翻译成一个可重定位目标文件(relocatable object file)main.o:
即:as [other arguments] -o /tmp/main.o /tmp/main.s
驱动程序经过相同的过程生成swap.o;
最后它运行链接器程序ld,将main.o和swap.o以及一些必要的系统目标文件组合起来,创建一个可执行目标文件(executable object file)p:
ld -o p [system object files ad args] /tmp/main.o /tmp/swap.o
要运行可执行文件p,可以在Unix的shell的命令行上输入它的名字:
unix> ./p
这时候shell会调用操作系统中一个叫做加载器的函数,它拷贝可执行文件p中的代码和数据到存储器中,然后将控制权转移到这个程序的开头。
===================================================
二、静态链接
像Unix ld程序这样的静态链接器(static linker)以一组可以重定位目标文件和命令行参数作为输入;
生成一个完全链接的可以加载和运行的可执行目标文件作为输出;
作为输入的可重定位目标文件由各种不同的代码和数据节(section)组成。
指令在一个节中、初始化的全局变量在另一个节中、未初始化的变量又在另一个节中;
为了构造可执行文件,链接器必须完成两个主要任务:
符号解析:目标文件定义和引用符号。符号解析的目的是将每个符号引用刚好和一个符号定义联系起来;
重定位: 编译器和汇编器生成从地址0开始的代码和数据节。然后编译器把每个符号定义和一个存储器位置联系起来。通过修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。 //这段听着很绕
目标文件纯粹是字节块的集合。链接器将这些块连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置。
链接器对目标机器了解甚少。产生目标文件的编译器和汇编器已经完成了大部分工作;
===================================================
三、目标文件
===================================================
四、可重定位目标文件
===================================================
五、符号和符号表
===================================================
六、符号解析
===================================================
七、重定位
===================================================
八、可执行目标文件
===================================================
九、加载可执行目标文件
===================================================
十、动态链接共享库
===================================================
十一、从应用程序中加载和链接共享库
===================================================
十二、与位置无关的代码
===================================================
十三、处理目标文件的工具
===================================================
十四、小结