【前言】大多数编译系统或者讲大多数程序需要经历预处理(器)、编译(器)、汇编(器)和链接(器)四个阶段。预处理会进行头文件的替换,编译器会将源文件汇编成汇编语言,汇编器将汇编语言翻译成二进制 的机器指令(被打包成一种二进制文件--可重定向的目标文件),链接器将已经经过以上三个步骤处理好的多个文件合并在一起。然后就得到一个可执行目标文件。着重讲一下链接器。
一、链接器的作用
链接器帮助实现---分离编译。多个.c文件各自生成.o文件,最后合并在一起,如此我们在改变某个.c时仅需要编译这部分文件就可以了,然后链接。若是头文件里的有#include标准库头文件,还会引入生成好的标准库.o函数,例如printf.o文件(头文件里面仅仅是声明,没有实现)--此时是静态库。(静态链接库和动态链接库,动态库把对一些库函数的链接载入推迟到程序运行的时期)
链接器由两个输入一组可重定向目标文件和命令行参数(-o),输出一个完全链接的、可以加载和运行的可执行目标文件。
二、目标文件的本质
在编译器前后我们看到了可重定向目标文件和可执行目标文件。这两个目标文件,还有动态链接库的共享目标文件这三个目标文件都是二进制代码和数据,纯粹的字节快的组合。目标文件是按照特定的目标文件格式组织的,各个系统各不相同。贝尔实验室第一个unix系统是a.out格式;现代x86-64linux的ELF格式。基本概念类似,以elf举例
(1)ELF文件头 (2)程序头表(可选) (3)第1节,第2节,...,第n节,... (4)节头表
二、链接器的两个任务
符号解析和重定位
1、符号解析
将重定向目标函数定义和引用的符号与一个符号的定义关联起来。每个符号对应一个函数、一个全局变量或者一个静态变量。对于全局和局部符号的对应,有强符号和弱符号之分,优先选择强符号,也就是我们讲的作用域范围;
对于静态库的符号对应,链接器只复制被程序引用的目标库(一般静态库将一个一个的函数分开好了制作成静态库,所以不大)。但是,对于多个进程静态库不共享需要各自复制一份,比较浪费内存,所以出现了共享库也叫动态链接库。库一般放在命令行结尾,若fun.c依赖x.a,而x.a依赖y.a 而y.a依赖x.a。则需要下面这样循环重复出现在命令行上。
linux>gcc fun.c libx.a liby.a libx.a
2、重定位
在结束符号解析之后,链接器就知道了输入模块中的代码节和数据节的大小。现在就开始重定位步骤了,在这个步骤中将合并输入模块,并为每个符号分配运行时地址。重定位分两步组成:
(1)重定位 节和符号定义
链接器将相同类型的节合并成同一个节,这个节就是可执行目标文件的节。然后,将运行时内存地址赋给新的节和每个符号定义。此时,程序中的每条指令和全局变量都有唯一的运行时内存了。
(2)重定位节中的符号引用
链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址,需要重定位条目数据结构。因为链接器不知道引用任何外部定义的函数或者全局变量的位置。具体可参考《深入理解计算机系统476页》比较复杂,能看懂。
三、加载可执行目标文件
Linux> ./prog 用加载器即可执行文件。若不加-lx.a(库名为libx.a),则默认去系统的lib下面和当前目录下去找,找不到报错。
Linux的执行过程就是4G内存模型,ELF格式的可执行目标文件的各种数据格式在内存模型四区里的代码段和数据段都有位置。具体加载器的工作原理:分配一个4G进程虚拟地址空间,代码段和数据段由可执行目标文件初始化,栈和堆初始化为0。