首页 > 代码库 > 编译原理

编译原理

        编译器的主要功能就是将高级语言翻译成机器语言的一个工具。平时我们对一个c文件执行gcc编译,其实该过程包括4个步骤,分别是预编译,编译,汇编,链接。

        例如对一个Hello World程序进行编译,编译过程如图:


                           


              此博文只对第二个步骤进行详细的分析,其他三个步骤请关注后期的博文。编译过程就是把预处理的文件进行一系列此法分析,语法分析,语义分析以及优化后生产相应的汇编代码文件。主要分为5部分,分别是:词法分析、语法分析、语义分析、中间语言生产和目标代码生产与优化。

        

        1. 词法分析:先是编译的文件作为一个输入文件输入到扫描器,由扫描器进行简单的词法分析。主要是将源代码的字符序列分割成一系列的记号。

      例如源代码:array[index] =(index + 4) * (2 + 6); 进行扫描器以后,产生16个记号如下图所示,

                       

            词法分析产生的记号一般可以分为:关键字、标识符、字面量(数字、字符串等)和特殊符号(加号、等号)。扫描器还完成将标识符存放到符号表,将数字、字符串常量存放到文字表等工作。(词法扫描又一个lex程序实现)

       

       2. 语法分析:语法分析有一个语法分析器完成,语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树。整个分析过程采用了上下文无关语法的分析手段。简单的说,由语法分析器生成的语法树就是以表达式为节点的树。源代码:array[index] = (index + 4) * (2 + 6);

       经过语法分析器以后形成的的语法树,如图:

                    


           整个语句被看作是一个赋值表达式,赋值表达式的左边是一个数组表达式,右边是一个乘法表达式。符号和数字是最小的表达式,所以通常作为整个语法树的节点。从上图还可以看出,语法分析过程还确定了运算符号的优先级和含义。比如,乘法表达式的优先级比加法高,圆括号表达式比乘法高;另外还确定含义,例如星号*在C语言中可以表示乘法表达式也可以表示指针,符号含义也在此阶段进行区分。

       

         3. 语义分析:语义分析由语义分析器完成,语义分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正的意义。比如,C语言里面两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的。编译器所能分析的语义是静态语义,即在编译期间可以确定的语义,而相对应的动态语义就是只是在运行期才能确定的语义。静态语义包括声明和类型的区配以及类型的转换。比如将一个浮点型赋值给一个指针的时候,语义分析程序会发现这个类型不区配,编译器将会报错。再如,将0作为除数是一个运行期间语义错误,这是动态语义问题。经过语义分析阶段以后,整个语法树的表达式都被标识类型,如果有些类型需要做转换,语义分析程序会在语法树中插入相应的转换节点。上例经过语义分析后成为的形式,如图:

                                           

        通过上图可以看到,每个表达式(包括符号和数字)都被标识了类型。

        

       4. 中间语言生成:编译器在源代码级别会有一个优化过程,由源码级优化器完成。源码级优化器在不同编译器中可能会有不同的定义或一些差异。上例中的(2+6)这个表达式可以被优化,因为它的值在编译期间就可以被确定。类似其他级别的代码优化这里不详细描述。经过优化的语法树如图:

                             

          表达式(2+6)优化成8,事实上是源代码优化器通过将整个语法树转换成中间代码,而不是直接在语法树上做优化的。中间代码已经非常接近目标代码,但是其与目标机器和运行环境都是无关的,例如它不包含数据的尺寸、变量地址和寄存器的名字等。中间代码有很多种类型,比较常见的有:三地址码和P-代码。

       下面分析三地址码,最基本的三地址码为:x = y OP z

       这个三地址码表示将变量y和z进行OP操作以后,赋值给x,这里OP操作可以是算数运算,也可以是其他任何应用到y和z的操作。三地址也由此得名。上例的语法树可以被翻译成三地址码后为:

        t1 = 2 + 6

        t2 = index + 4

         t3 = t2 * t1

         array[index] = t3

         为了是操作都符合三地址码形式,上例利用三个临时变量t1、t2和t3.在三地址码的基础上进行优化得到t1 = 8。然后把8替换t1,因为t2可以重复利用,最后优化的结果为:

        t2 = index + 4

        t2 = t2 * 8

        array[index] = t2

        中间代码可以说是编译过程的一个分水岭,编译器前段负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。从中可以看出,中间代码之前的代码在任何机器都是通用的。

      

        5.目标代码生成与优化:处理完源代码后接下来都是属于编译器后端的内容。后端主要包括代码生成器和目标代码优化器。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等。对于上例子中的中间代码,经过代码生成器后可能会生成如下代码(不同平台有所不同,以下是X86平台):

movl index ,%ecx

addl $4,%ecx

mull $8,%ecx

movl index ,%ecx

movl %ecx,array(,eax,4)

       最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式、使用移位来代替乘法运算、删除多余的指令等。上面的例子,乘法由一条相对复杂的基址比例变址寻址的lea指令完成。最后优化为:

movl index ,%edx

leal 32(,%edx,8),%eax

movl %eax,array (,%edx,4)

编译原理