在我们平常的应用程序开发的过程中,我们很少去关注编译和链接的过程,因为通常的开发环境都是集成的开发环境(IDE)
首先,我们编写一个"hello word”的程序如下:
在Linux下,当我们使用gcc来编译程序如下:
实际上,上述过程,我们可以将其分为4个步骤
分别是预处理、编译、汇编和链接
一、预编译
对上述程序进行预编译,生成一个.i文件
预编译主要处理那些源代码文件中以“#”开始的预编译指令,比如“#define”、“#include”,主要处理规则如下:
- 将所有的“#define”删除,并且展开所有的宏定义
- 处理所有的条件预编译指令,比如“#if”、“#ifdef”、“endif”
- 处理“#include”预编译指令,这个过程是一个递归的过程,因为被包含的文件可能还包含其他文件
- 删除所有的注释
- 添加行号和文件名标识
-
保留所有的**#pragma编译器指令**
综上,可以看出,当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题
二、编译
编译过程如下:生成的.s文件
补充:现在gcc把预编译和编译两个过程合并成了一个步骤,使用一个叫做ccl的程序完成。
主要进行步骤分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。
以程序 array[index] = (index + 4) *(2+6)为例子,加以分析:
1、语法分析:
首先源代码程序被输入到扫描器,进行词法分析,运用类似于邮箱状态机的算法将原地阿妈的字符序列分割成一系列的记号。如下图:
词法分析的记号一般可以分为如下几类:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(如加号、等号)
2、语法分析
语法分析器产生语法树——以表达式为节点的树
如果出现了表达式不合法,比如各种括号不匹配、表达式中缺少操作符等,编译器就会报告语法分析阶段的错误
3、语义分析
由语义分析器来完成。前面的语法分析仅仅完成了对表达式的语法层面分析,但是他并不了解这个语句是否真正有意义。
编译器能分析的语义是静态语义——通常包括申明和类型的匹配,类型的转换。那么与之对应的就是动态语义——在运行的期才能确定的语义,比如0作为除数是一个运行期语义错误。
经过语义分析阶段过后,整个语法树的表达式都被表示了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。
4、中间语言生成
源码级优化器会在源代码级别进行优化。其往往将整个语法树转成中间代码。
中间代码有很多种类型,比较常见的有——三地址码
x = y op z (这里的op操作可以是算术运算,可以是任何可以应用到y和z的操作。优化后的语法树如下:
中间代码使得编译器可以被分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。
5、目标代码生成与优化
目标代码优化器对上述的目标带啊进行优化,比如选择合适的寻址方式、使用唯一来代替乘法运算、删除多余的指令等
三、汇编
汇编器将汇编代码庄边成机器可以执行的指令。每一个汇编语句都对应一条机器指令。实现过程如下:
汇编器的汇编过程相对于编译器来讲比较简单,没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。
四、链接
链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地链接。
比如一个复杂的软件,人们把每个源代码模块独立的编译,然后按照需要将他们“组装”起来,这个组装模块的过程就是链接。过程如下:
每个模块的源代码文件(如.c)文件经过编译器编译成目标文件(.o或.obj),目标文件和库一起链接形成最终的可执行文件。
链接过程主要包括了地址和空间分配、符号决议和重定位等这些步骤。