静态链接
大多数编译系统提供编译器驱动程序( compiler driver ),它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。比如 GCC (GNU Compiler Collection) GUN编译器套件,其中 GUN 是一个开源组织,而 GCC 是他们开发的一款编译器套件,应用十分广泛。
链接 是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接前 —— 预处理、编译
C 的源代码。在编写出来后是不能直接被运行的 —— 其实在完成编写后只是一个文本文件而已。
complie.jpg源代码需要经过:预处理、编译和汇编这一过程后产生目标文件
-
预处理 :在这个阶段会对源代码进行插入修改等。主要进行:头文件的插入(
#include
)、宏的替换(#define
)、根据条件编译指令(#ifdef
、#ifndef
、#else
、#elif
、#endif
)过滤掉没必要的代码和识别特殊符号进行替换。 - 编译:主要是由编译器完成。主要经过:词法分析、语意分析和中间代码生成。一般来说会输出汇编代码
- 汇编:主要是汇编器完成。输入是有编译器输出的中间文件 —— 主要为汇编代码,输出机器语言文件。
目标文件格式
在介绍链接之前需要对链接的目标 —— 目标文件,的格式有所了解才可以清晰的理解链接的过程。
每个操作系统都有自己的目标文件的格式。这里便于理解主要采用 Linux 系统下的目标文件 ELF(Executeable Linkable Format) 做介绍。
程序的本质就是对数据的处理。所以一段程序可以分成两大部分:数据和指令。其中指令就是代表对数据的操作。所以在 ELF 中是分节储存的。如图:
elf_format.jpg上图中除了* File Header (文件头)以外,还有 .text section、.data section* 和 .bss section 。
当然上图只是简化版的其中还有许多的其他的节,为了便于理解就省略了其他的节 。
其中 File Header 它描述了整个文件的文件属性,包括文件的类型,以及目标操作系统的信息等。其中有一个重要的信息就是节表,节表保存了各个 节的位置以及属性等节的信息。
.text
节 (.text section) 保存了执行语句的机器代码。.data
节 (.data section) 保存了已经初始化的全局变量和局部静态变量。.bss
节(.bss section) 保存了未初始化的全局变量和局部静态变量。所以总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。.text
属于程序指令,而 .bss
和 .data
属于程序数据。
除了图中出现的节以外,目标文件还会包含一个符号表。符号表包含了该模块所定义和引用的所有符号。
符号:每个符号对应于一个函数、一个全局变量或一个静态变量(在C语言中用 static 申明的变量)。
链接
链接主要由链接器完成。链接器主要有两大任务:
- 符号解析:将每个符号引用正好和一个符号定义关联起来
- 重定位:编译器和汇编器生成的是地址从0开始的代码和数据。通过把符号定义与一个确切的内存地址相关联,从而修改这个符号引用的地方指向与其符号定义相关联的内存地址。
符号解析
符号表是由汇编器构造的,使用编译器输出到汇编语言.s 文件中的符号。每个系统上符号表的数据结构都是不同的。
链接器解析符号引用的方法是讲每个引用与它输入的目标文件的符号表中的一个确定的符号定义关联起来。
对于多重定义的全局符号,链接器有他们自己的规则来处理多重定义的符号名比如:
- 不允许有多个同名的强符号
- 如果有一个强符号和多个弱符号同名,那么选择强符号
- 如果有多个弱符号同名,那么从这些若符号中任意选择一个
强符号:函数和已经初始化的全局变量
弱符号:未初始化的全局变量
当链接器解析和静态库相关的符号时,链接器会创建:文件集合E、未解析符号集合U和已定义符号集合D。
- 对于输入的文件f进行判断是目标文件还是存档文件(库文件):如果f是目标文件,那么链接器吧f添加到E,在根据f中的符号引用的关系来修改U和D。
- 如果f是一个存档文件(库文件),那么链接器就尝试匹配U中未解析的符号和有存档文件(库文件)成员定义的符号。如果存档文件(库文件)中某个成员m,定义了U集合中的符号,那么就将m添加到E中,根据m的符号来修改U和D集合。对于存档文件(库文件)中的所有成员进行以上的操作,知道U和D集合都不在改变,这个时候将没在E集合中的成员丢弃。
- 当链接器完成所有文件上的扫描以后,如果U集合是非空的话那么就输出错误链接终止。如果是空的那么就合并和重定位E集合中的文件,构建输出的可执行文件。
重定位
在完成了符号解析以后,此时,链接器就知道了整个模块的确切大小。那么就可以开始重定位步骤,这个步骤主要由两个部分组成:
- 重定位节和符号定义:合并各个模块中相同的节合并为同一类型的聚合节 。以及吧运行时内存地址赋给新的聚合节和模块中的给个符号。
- 重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
之前介绍过,在编译器生成目标文件时候,有许多不同的数据节和代码节用来存放不同的代码和变量等。比如: .data
节是用来存放已经初始化的全局和静态变量,.text
节是用来存放已编译的程序的机器代码,.bss
节是存放未初始化的全局变量和静态变量。而在重定位节和符号定义的步骤中,链接器会吧各个目标模块的这些数据节和代码节合并成一个数据节或者是代码节。如多个 .data
节会合并成一个 .data
节。同时在这个步骤中链接器也会赋给输入模块定义的每个符号给地址,这样到这一步骤完成时,程序中的每条指令和全局变量都有唯一的运行时内存了。
在所有的符号都有了唯一的运行时内存后,接下来就是要替换掉相关的符号引用的以地址来代替了。在汇编器生成目标模块的时候任何对外部符号的引用的地方都会生成一个叫重定位的条目,而链接器则会根据这个重定位的条目来替换想对应得地址。这就就合并成了一个可执行文件。
到此为止,静态链接的部分已经完成。剩下的就是把可执行文件装载进内存然后运行。同时动态链接和静态链接也是不同的。