这一节将对狭义的编译的内部处理过程进行介绍。 编译的4 个阶段 狭义的编译大致可分为下面4 个阶段。 1. 语法分析 2. 语义分析 3. 生成中间代码 4. 代码生成 下面就依次对这4 个阶段进行说明。 语法分析 一般我们所说的编写程序,就是把代码写成人类可读的文本文件的形式。像C 和Java 这样,以文本形式编写的代码对人类来说的确易于阅读,但并不是易于计算机理解的形式。因此,为了运行C 和Java 的程序,首先要对代码进行解析,将其转换为计算机易于理解的形式。这里的解析(parse)也称为语法分析(syntax analyzing)。解析代码的程序模块称为解析器(parser )或语法分析器(syntax analyzer)。那么“易于计算机理解的形式”究竟是怎样的形式呢?那就是称为语法树(syntax tree)的形式。顾名思义,语法树是树状的构造。将代码转化为语法树形式的过程如图1.3 所示。 图1.3 语法树 语义分析 通过解析代码获得语法树后,接着就要解析语法树,除去多余的内容,添加必要的信息,生成抽象语法树(Abstract Syntax Tree,AST)这样一种数据结构。上述处理就是语义分析(semantic analysis)。 语法分析只是对代码的表象进行分析,语义分析则是对表象之外的部分进行分析。举例来说,语义分析包括以下这些处理。 ●●区分变量为局部变量还是全局变量 ●●解析变量的声明和引用 ●●变量和表达式的类型检查 ●●检查在引用变量之前是否进行了初始化 ●●检查函数是否按照定义返回了结果 上述处理的结果都会反映到抽象语法树中。语法分析生成的语法树只是将代码的构造照搬了过来,而语义分析生成的抽象语法树中还包含了语义信息。例如,在变量的引用和定义之间添加链接,适当地增加类型转换等命令,使表达式的类型一致。另外,语法树中的表达式外侧的括号、行末的分号等,在抽象语法树中都被省略了。 生成中间代码 生成抽象语法树后, 接着将抽象语法树转化为只在编译器内部使用的中间代码(Intermediate Representation,IR)。 之所以特地转化为中间代码,主要是为了支持多种编程语言或者机器语言。例如,GCC 不仅支持C 语言,还可以用来编译C++ 和Fortran。CPU 方面,不仅是Intel 的CPU,还可以生成面向Alpha、SPARC、MIPS 等各类CPU 的机器语言。如果要为这些语言和CPU 的各种组合单独制作编译器,将耗费大量的时间和精力。Intel CPU 用的C 编译器、IntelCPU 用的C++ 编译器、Intel CPU 用的Fortran 编译器、Alpha 用的C 编译器……要制作的编译器的数量将非常庞大(图1.4)。 图1.4 不使用中间代码的情况而如果将所有的编程语言先转化为共同的中间代码,那么对应一种语言或一种CPU,只要添加一份处理就够了(图1.5)。因此支持多种语言或CPU 的编译器使用中间代码是比较合适的。例如GCC 使用的是一种名为RTL(Register TransferLanguange)的中间代码。 根据编译器的不同,也存在不经过中间代码,直接从抽象语法树生成机器语言的情况。本书制作的C♭ 编译器最初并没有使用中间代码,后来发现使用中间代码的话,代码的可读性和简洁性都要更胜一筹,所以才决定使用中间代码。解析代码转化为中间代码为止的这部分内容,称为编译器的前端(front-end)。 图1.5 使用中间代码的情况 代码生成 最后把中间代码转换为汇编语言,这个阶段称为代码生成(code generation)。负责代码生成的程序模块称为代码生成器(code generator)。 代码生成的关键在于如何来填补编程语言和汇编语言之间的差异。一般而言,比起编程语言,汇编语言在使用上面的限制要多一些。例如,C 和Java 可以随心所欲地定义局部变量,而汇编语言中能够分配给局部变量的寄存器只有不到30 个而已。处理流程控制方面也只有和goto语句功能类似的跳转指令。在这样的限制下,还必须以不改变程序的原有语义为前提进行转换。 优化 除了之前讲述的4 个阶段之外,现实的编译器还包括优化(optimization)阶段。现在的计算机,即便是同样的代码,根据编译器优化性能的不同,运行速度也会有数倍的差距。由于编译器要处理相当多的程序,因此在制作编译器时,最重要的一点就是要尽可能地提高编译出来的程序的性能。 优化可以在编译器的各个环节进行。可以对抽象语法树进行优化,可以对中间代码的代码进行优化,也可以对转换后的机器语言进行优化。进一步来说,不仅是编译器,对链接以及运行时调用的程序库的代码也都可以进行优化。 总结 经过上述4 个阶段,以文本形式编写的代码就被转换为了汇编语言。之后就是汇编器和链接器的工作了。 本书中所制作的编译器主要实现上述4 个阶段的处理。