本章导语
日常应用程序开发,我们很少需要关注编译和链接过程,IDE一般都将编译和链接的过程一步完成,形成 构建(Build)。但此过程中,我们往往会被这些复杂的集成工具所提供的强大功能所迷惑,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们需要透过现象看本质,深入理解整个过程的机制,才能游刃有余的解决这些问题。
2.1 被隐藏的过程
GCC编译代码文件到最终的可执行程序的过程可以分解为4个步骤,分别是:
预编译(Prepressing) -> 编译(Compilation) -> 汇编(Assembly) -> 链接(Linking)
2.1.1 预编译(Prepressing)
预编译过程主要处理那些源代码文件中的以‘#’开头的预编译指令。比如“#include”、“#define”等,其处理规则如下:
- 将所有的 “#define” 删除,并且展开所有的宏定义
- 处理所有条件预编译指令,比如 “#if”,“#ifdef”,“#elif”,“#else”,“#endif”
- 递归进行处理 **“#include”**预编译指令,将被包含的文件插入到该预编译指令的位置。
- 删除所有的注释 **“//”**和 “/ /”
- 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
- 保留所有的 “#pragram” 编译器指令,因为编译器须要使用它们。
gcc -E hello.c -o hello.i
或者cpp hello.c > hello.i
- 当无法判断宏定义是否正确或头文件是否包含正确时,可以查看预编译后的文件来确定。
2.1.2 编译(Compilation)
编译过程主要把预编译完后的文件进行一系列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件。
gcc -S hello.i -o hello.s
或者cc1 hello.c
2.1.3 汇编(Assembly)
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应着一条机器指令。
gcc -c hello.[sc] -o hello.o
或者as hello.s -o hello.o
2.1.4 链接(Linking)
链接器是将目标文件(.o文件)与库文件链接起来形成最终的可执行文件的过程。会有一节来详细介绍。
2.2 编译器做了什么
最直观的角度来讲,编译器就是将高级语言翻译成机器语言的一个工具。编译过程一般分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。
2.2.1 词法分析
源代码程序被输入到扫描器(Scanner),运用类似于 有限状态机(Finite State Machine) 的算法可以很轻松地将源代码的字符序列分割成一系列的 记号(Token)。
词法分析产生的几号一般可分为如下几类:关键字、标志符、字面量(包含数字、字符串等)和特殊符号(如加号、赋值号)。在识别标记的同时,也完成了如下的操作:
- 将标志符存放到符号表
- 将数字、字符串常量存放到文字表
以备后续步骤使用。
词法扫描工具 lex
,会按照用户之前描述好的词法规则将输入的字符串分割成一个个几号。
2.2.2 语法分析
语法分析(Grammar Parser)将由扫描器产生的标记进行语法分析,从而产成 语法树(Syntax Tree)。整个分析过程采用 **上下文无关语法(Context-free Grammar)**的分析手段。简单地讲,由语法分析器生成的语法树就是以 表达式(Expression) 为节点的树。
语法分析工具 yacc(Yet Another Compiler Compiler)
,它也像lex一样,可以根据用户给定的语法规则对输入的记号序列进行解析,从而构建出一棵语法树。
2.2.3 语义分析
语义分析(Semantic Analyzer)器,所能分析的语义是 静态语义(Static Semantic,编译期间可以确定的语义),它通常包括声明和类型的匹配,类型的转换。
2.2.4 中间语言生成(略.)
2.2.5 目标代码生成与优化(略.)
2.3 链接器年龄比编译器长
在现代的大型软件往往拥有成千上万个模块,这些模块之间相互依赖又相对独立。开发过程中,按照层次化及模块化存储和组织源代码有很多好处,比如代码更容易阅读、理解、重用,每个模块可以单独开发、编译、测试,改变部分代码不需要编译整个程序等。在这种情况下,如何组合这些分割开来的模块形成一个单一的程序是必须解决的问题。
- 模块之间如何组合的问题可以归结成模块之间如何通信的问题
- 最常见的属于静态语言的C/C++之间通信有两种方式,一种是模块间的函数调用,另外一种是模块间的变量访问。
函数访问须知道目标函数的地址,变量访问也须知道目标变量的地址,所以归结为一种方式,那就是模块间符号的引用
。模块间依靠符号来通信类似于拼图板,定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域,两种一拼接刚好完美组合
。这个模块拼接的过程就是 链接(Linking)。
2.4 模块拼装 - 静态链接
程序设计的模块化是人们一直在追求的目标,将一个复杂的系统逐步分割成小的系统以达到各个突破的目的
。人们把每个源代码模块独立地编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是 链接(Linking)。
- 链接的主要内容就是
把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接
。- 链接器工作实质上即是
把一些指令对其他符号地址的引用加以修正
。- 链接过程主要包括:
- 地址和空间分配(Address and Storage Allocation)
- 符号决议(Symbol Resolution)/符号绑定(Symbol Binding)/地址绑定(Address Binding)
- 重定位(Relocation)
- 基本的静态链接过程:
每个模块的源代码文件警告编译器编译成目标文件,目标文件和库一起链接成最终可执行文件
。- 最常见的库就是 运行时库(Runtime Library),它是支持程序运行的基本函数的集合。库其实是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放。