一、编译
编译器的编译过程一般分为6步:扫描(Scanner),语法分析(Grammar Parser),语义分析(Semantic Analyzer),源代码优化(Source Code Optimizer),代码生成(Code Generator),目标代码优化(Target Code Optimizer)。
array[index] = (index+4)*(2+6);
1、词法分析
首先源代码将输入到扫描器,扫描器的任务很简单,它只是简单地进行词法分析,将源代码的字符序列分割成一系列的记号,上述代码经过扫描后可以产生16个记号如下。
array | 标识符 |
[ | 左方括号 |
记号 | 类型 |
index | 标识符 |
] | 右方括号 |
= | 赋值 |
( | 左圆括号 |
index | 标识符 |
+ | 加号 |
4 | 数字 |
) | 右圆括号 |
* | 乘号 |
( | 左圆括号 |
2 | 数字 |
+ | 加号 |
6 | 数字 |
) | 右圆括号 |
词法分析产生的记号一般可以分为如下几类:关键字,标识符、字面量(包含数字,字符串等)和特殊符号(如加号、等号等)。在识别记号的同时,扫描器还完成了其他工作,比如将标识符存放到符号表,将数字、字符串敞亮存放到文字表等,以备后边的步骤使用。对于C语言而言,它的宏替换和文件包含工作不属于编译器工作范畴,而交给了一个独立的预处理器,暂且不表。
2、语法分析
接下来语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树(Sytax Tree)。整个分析过程采用了上下文无关文法,由语法分析器生成的语法树就是以表达式为节点的树。(忘了,后续查看具体实现过程)
3、语义分析
语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义,比如C语言中两个指针做乘法运算是没有意义的,但是这个语句在语法分析上是合法的。编译器所能分析的语义是静态语义,所谓静态语义是指在编译期可以确定的语义,与之对应的动态语义却只有在运行期才能确定的语义。
静态语义通常包括声明和类型的匹配,类型的转换。比如当一个浮点型的表达式赋值给一个整形的表达式时,其中隐含了一个浮点型到整形转换的过程,语义分析过程中需要完成这个5步骤。比如将一个浮点型赋给一个指针的时候,语义分析程序会发现这个类型不匹配,编译器就会报错。动态语义一般指在运行期出现的语义相关的问题,比如将0作为除数是一个运行期的语义错误。
经过语义转换阶段以后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。
4、中间语言生成
现代的编译器往往有着很多层次的优化,往往在源代码级别会有一个优化过程。这里所描述的源码级优化器在不同编译器中可能会有不同的定义或者有一些其他的差异。源代码级优化器会在源代码级别进行优化,如上例中(2+6)这个表达式可以被优化掉,因为它的值在编译期就可以被确定。经过源代码优化器优化之后会生成一个新的语法树,在上例中,(2+6)这个表达式就被优化为8。直接在语法树上做优化显得很困难,所以源代码优化器往往将整个语法树转换为中间代码,它是语法树的顺序表示,其实它已经非常接近目标代码了,但是它一般跟目标机器和运行时的环境是无关的,比如它不包含数据的尺寸,变量地址和寄存器的名字。中间代码有很多类型,在不同编译器中有着不同的形式,比较常见的有三地址码和P-代码,我们就拿最常见的三地址码作为例子,最基本的三地址码是这样的:
x = y op z ,这个三地址码表示将变量y和z进行op操作后。赋值给x,这里的op操作可以是算术运算,也可以是其他任何二元操作符,因为一个三地址码语句包含了三个变量地址,即:t1 = 2+6 t2 = index + 4 t3 = t2 * t1 array[index] = t3 ,这里我们将三地址码作为中间类型,相应的需要利用几个临时变量。此时在对三地址码形式的优化会把2+6的值算出来,得到优化。
中间代码使得编译器可以被分为前段和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换为目标机器代码。这样对于一些可以跨平台的编译器而言,它们可以针对不同的平台而使用同一个前端和针对不同机器平台的数个后端。
5、目标代码生成和优化
源代码级优化器产生的中间代码标志着下面的过程都属于编译器后端,编译器后端主要包括代码生成器和目标代码优化器。
代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器。上例中最后的中间代码,代码生成器会生成下面的代码序列:
movl index, %ecx; // ecx = index
addl $4, %ecx; // ecx = ecx + 4
mull $8, %ecx; // ecx = ecx * 8
movl index, %eax; //eax = index
movl %ecx,array(,eax,4); //array[index] = ecx
最后目标代码优化器对上述目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等。
经过这些扫描、语法分析,语义分析、源代码优化,代码生成,目标代码优化过程,源代码被编译成了目标代码。
二、链接
链接的主要内容就是把各个模块之间的相互引用的部分处理好,使得各个模块之间能够正确的衔接,链接的过程主要包括了地址和空间分配、符号决议和重定位等这些步骤
现代的编译和链接过程也并非想象之中那么复杂,它还是一个比较容易理解的概念。如我们在程序模块main.c中使用另一个模块fun.c中的函数foo()。我们在Main.c模块中每一处调用时候都必须确切知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候并不知道foo这个函数的地址,所以它暂时把这些调用foo的指令的目标搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。如果没有链接器需要我们手工的把每个调用foo的指令进行修正,填入正确的foo()地址。使用链接器可以直接引用其他模块的函数和全局变量而无需知道它们的地,因为链接器在链接的时候,会根据你所引用的符号去相应的func.c模块查找foo的地址,然后将main.c模块中所有引用到foo的指令重新修正,让他们的目标地址为真正的foo函数的地址。这就是静态链接的最基本的过程和作用。
这个地址修正的过程叫做重定位,每个要修正的地方叫做重定位入口。