编译器的主要作用就是将高级语言编译成机器语言的一个工具;在编译器出现之前,程序员都忙碌于使用机器语言或者汇编语言编程,这是一个复杂而又繁琐的工作。同时,编写的程序只能在特定的CPU环境下运行,如果换一种CPU环境,又需要重新开发,这几乎是让人不能接受的。到了上个世纪六七十年代,出现了很多高级编程语言,比如C++和Fortran,这些高级语言的出现,使得程序员能够根据专注于程序逻辑的本身,而尽量减少对计算机本身的考虑,高级语言虽然出现了,但是它不能够直接在计算机上运行,它需要转换成机器语言后,才能够被计算机所识别运行,这个时候就需要编译器了;编译器的主要功能就是将高级语言转成对应的机器代码。
编译器的编译过程主要可以分为扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化这6大步。现在我们就这6步进行逐个介绍。
1、扫描(也叫词法分析)
首先编译器调用扫描器,扫描源代码,并进行词法分析,将源代码的字符序列分隔成一堆记号。举个例子:
array[index] = (index+4)*(2+6);
CompilerExpression.c
上面的那行代码中包含了28个非空字符,经过扫描以后产生16个符号
词法分析产生的记号一般分为如下几类:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(比如加号、减号等);在表示记号的同时,扫描器也完成了其他的工作,比如将标识符存放到符号表,将数字存放到文字表等。常见的词法分析程序包括lex程序等。当然,对于一些有预处理的语言,比如C/C++,不能一上来就直接对文件进行这样分析,还需要进行一个预处理过程,包括消除注释、宏展开等等。
2、语法分析
在扫描器完成词法分析过程后,接下来就轮到语法分析器工作了;语法分析器对产生的记号进行语法分析,生成对应的语法树,整个语法分析过程,采用了上下文无关语法的分析手段。通过语法分析器生成以表达式为节点的树,即语法树;
如上图,符号和数字是最小的表达式,他们不是由其他的表达式组合而成的,所以他们通常为整个语法树的叶节点;在语法分析的同事,很多运算符号的优先级和含义也都被确定下来了。另外,有些符号具有多重含义,比如*可以为取指针内容,也可以是乘号,在这个时候也要确定下来他们的具体含义。如同前面词法分析器有lex,语法分析器也有一种专门的工具,叫做yacc( Yet another complier complier ), 简称“编译器编译器”。
3、语义分析
语义分析由语义分析器完成。语法分析仅仅是对表达式层面的语法分析,但它不是真的了解这个语法是否真正有意义。比如C语言里面,两个指针做乘法运算是没有意义的,但是在语法层面却是合法的。编译器所能分析的是静态语义;所谓静态语义是指在编译期可以确定的语义,与之对应的是动态语义。 静态语义通常包含申明和类型匹配、类型转换、比如类型的隐示转换。
经过语义转换以后,整个语法树的表达式都被标上了类型,如果有需要做隐式转换的,语义分析器会在语法树中插入相应的节点。上面的2.3图在经过语法分析器分析后,变成如下图所示:
4、源代码优化
源代码优化,也称作为中间语言生成,现代的编译器有着很多层次的优化,往往在源代码级别就开始进行了优化。在不同的编译器中,源代码优化器有着不同的定义。源代码优化器会在源代码级别进行优化,比如上面例子中的(2+6)表达式会被直接优化掉,因为它的赋值直接在编译器就可以确定,不需要等到运行期。类似的还有很多其他的复杂的优化过程,这里就不一一陈述了。
其实,在真实的源代码优化器中,直接在语法树上进行优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码;中间代码,即语法树的顺序表达,其实已经非常接近目标代码了,但它跟运行的目标机器已经运行环境无关,比如它不包含数据的地址,数据的尺寸等等。中间代码在不同的编译器中可能有着不同的类型,常见的类型有三地址码和P—代码;
以三地址码为例,最基本的三地址码是这样的:x = y op z,这个三地址码表示将 y 和 z 进行 op操作,然后赋值给x;上面的例子中的语法树可以被翻译成三地址码后是这样的。t1 = 2+6
t2 = index + 4
t3 = t2 * t1
array[index] = t3
可以看到,这里为了使所有的代码都符合三地址码,我们加入了一些临时变量t1、t2 等,在三地址码的基础上进行优化,将2 + 6 的结果计算出来,得到t1 = 8,然后在后面的代码中t1用数字8替换,省去了一个临时变量。同时,t3也是可以替换的,通过重复利用t2变量,进行优化得到下面的代码
t2 = index + 4
t2 = t2 * 8
中间代码使得编译器可以被分为前端和后端,编译器前端负责产生机器无关的中间代码,编译器后端负责将中间代码转换成目标机器代码。
5、目标代码生成及优化
这里直接将后面两步目标带生成、目标代码优化归到一块讲,他们都可以归属到编译器后端。编译器后端主要包括代码生成器和目标代码优化器。代码生成器主要负责将中间代码转换成目标机器代码,这个过程比较依赖于目标机器,因为不同的机器有着不同的字长,寄存器、整数数据类型等等,
最后目标代码优化器对上面生成的目标代码进行优化,比如选择合适的寻址方式,使用移位操作来代替乘法、删除多余的指令等。
现代的编译器有着非常复杂的结构,这是因为高级编程语言本身非常复杂,比如C++语言本身定义就非常复杂,现在仍然没有一个编译器能够完整支持C++标准语言所规定的所有语言特性。同时,现代的计算机CPU也是非常的复杂,CPU本身采用了诸如流水线、多发射、超标量等复杂特性。为了支持这些特性,编译器的机器指令优化过程也变得复杂。使得编译更为复杂的是,有些编译器可以同时支持多种不同的硬件环境,比如著名的GCC就几乎能够支持所有的CPU平台。