当在Xcode中对一份源码按下command+R(Run)时,实际上做了以下四部操作:预处理(Prepressing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。
预处理:预处理过程主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include”、“#define”、注释" //、/**/ ”等。
编译:编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件。
汇编:汇编是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
链接:将目标文件链接起来形成可执行文件
编译器
编译过程一般可以分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。整个过程如图1所示:
1、扫描:首先源代码程序被输入到扫描器(Scanner),扫描器的任务很简单,它只是简单地进行词法分析,运用一种类似于有限状态机(Finite State Machine)的算法可以很轻松地将源代码的字符序列分割成一系列的记号(Token)。
比如图2这段代码通过扫描之后,就会被生成如图3这样的表:
2、语法分析:接下来语法分析器(Grammar Parser)将对由扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree)。以图2中的代码为例会生成图4这样的语法树。
3、语义分析:经过语义分析阶段以后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。图4语法树将会成为图5所示语法树的形式。
4、源代码优化:顾名思义就是对源码进行优化,优化后的语法树如图6所示。仔细观察,会发现(2 + 6)这个表达式已经被优化掉,因为这个值在编译期就可以确定。其实直接在语法树上作优化是比较困难的,所以源代码优化器往往将整个语法树转化成中间代码,它是语法树的顺序表示,其实它已经非常接近目标代码了。但是它一般跟目标机器和运行时环境是无关的,比如它不包含数据的尺寸、变量地址和寄存器的名字。中间代码使得编译器可以被分为前端和后段。编译器前端负责产生机器无关的中间代码,编译器后段负责将中间代码转化成目标机器代码。
5、目标代码生成和优化:源代码级优化器产生中间代码标志着下面的过程都属于编辑器后端。编译器后端主要包括代码生成器(Code Generator)和目标代码优化器(Target Code Optimizer)。让我们先来看看代码生成器。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等。最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等。
链接器
链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)
链接过程如图6所示:
举个例子:比如我们在程序模块main.c中使用另外一个模块func.c中的函数foo()。我们在main.c模块中每一处调用foo的时候都必须确切知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候它并不知道foo函数的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。如果没有链接器,须要我们手工把每个调用foo的指令进行修正,则填入正确的foo函数地址。当func.c模块被重新编译,foo函数的地址有可能改变时,那么我们在main.c中所有使用到foo的地址的指令将要全部重新调整。这些繁琐的工作将成为程序员的噩梦。使用链接器,你可以直接引用其他模块的函数和全局变量而无须知道它们的地址,因为链接器在链接的时候,会根据你所引用的符号 foo,自动去相应的func.c模块查找foo的地址,然后将main.c模块中所有引用到foo的指令重新修正,让它们的目标地址为真正的foo函数的地址。这就是静态链接的最基本的过程和作用。