1.0 本章概述
1.1 参考教材和相关课程
主要教材是第一本:
Compilers Principles, Techniques and Tools
,《编译原理》本科教学版,机械工业出版社(龙书)Compiler Construction: Principles and Practice
,编译原理与实践,Kenneth C.Louden,机械工业出版社- 《编译原理》,何炎祥,华中科技大学出版社
- 《程序设计语言编译原理》,陈火旺等,国防工业出版社
国外高校的相关课程(课程的层次设计):
- Stanford (Jeffrey Ullman、Monica Lam)•
- CS143: Compilers
- CS243: Advanced Compiling Techniques
- UC Berkeley
- CS164: Programming Languages & Compilers
- CS264: Implementation of Programming Languages
- UC Irvine
- CS/E142: Compilers & Interpreters
- CS/E141:Programming Languages
1.2 学习目标和课程内容
学习目标:本课程是计算机类专业的专业课,目的是使学生对形式语言有初步了解,熟悉编译程序的整个结构,了解并掌握编译程序的基本理论和方法,具有分析和实现编译程序的初步能力。
课程内容:
- 编译器的一般原理和基本实现方法(第一章)
- 文法和形式语言(第二章)
- 有穷自动机理论(第三章)和词法分析(第四章)
- 语法制导的定义和属性文法
- 类型论和类型系统
- 语法分析(第五章——第七章)
- 语义分析和中间代码生成(第八章)
- 运行阶段的存储组织与分配(第九章)
- 掌握和了解符号表的构造(第十章)
- 中间代码优化(第十一章)
- 目标代码的生成(第十二章)
学习这门课的意义所在:
- 对和编程语言有关的理论有所了解;
- 对编程语言的设计和实现有深刻的理解;
- 从软件工程来看,编译器是一个很好的实例,所介绍的概念和技术能够应用到一般的软件设计中。
1.1 程序的翻译
1.1.1 程序设计语言和翻译程序
编程语言层级:
- 机器语言(Machine language)是计算机代码形式的指令系统;
- 汇编语言(Assemble language)是计算机符号形式的指令系统;
- 高级程序设计语言:把诸如
BASIC,Algol,Fortran,Cobol,Pascal,Lisp,C, Prolog, Ada,Java
等统称为高级语言,它们一般不依赖于具体的计算机。与此相比,前者又称为低级语言。
问题是:计算机只能识别二进制数 0, 1
表示的指令和数构成的本计算机系统的机器语言。如何让计算机执行高级语言程序呢?必须使用翻译程序。所谓翻译程序是指这样一种程序,它能将用甲语言(源语言)编写的程序,翻译成等价的用乙语言(目标语言)书写的程序。程序的翻译通常有两种方式:一是 编译 方式,二是 解释 方式。
1.1.2 编译方式及特点
编译方式是一种分阶段进行的方式,源程序的编译和目标程序的运行是分成两个阶段完成的。编译方式的特点:
-
源程序的执行需要分阶段。
- 如果目标程序是机器语言程序,两大阶段:编译阶段和运行阶段。
- 如果目标程序是汇编语言程序,三大阶段:编译阶段、汇编阶段和运行阶段。
- 如果目标程序是机器语言程序,两大阶段:编译阶段和运行阶段。
-
编译方式生成了目标代码,且可多次执行。
关于编译程序的几点说明:
- 编译程序生成的目标程序不一定是机器语言的程序,也有可能是汇编语言程序;
- 编译程序与具体的机器和语言有关,即任何一个具体的编译程序都是某一特定类型的计算机系统中关于某一特定语言的编译程序;
- 对编译程序而言,源程序是输入数据,目标程序是输出结果。
1.1.3 解释方式及特点
解释程序不先产生目标程序然后再执行,而是按被解释的源程序的逻辑流程,逐句地分析解释,并立即予以执行。按照解释方式进行翻译的翻译程序称为解释程序。
比较解释方式和编译方式,在解释方式下,并不生成目标代码,而是直接执行源程序所指定的运算,这是编译方式与解释方式的根本区别。相同点是,解释器和编译器都需要对源程序进行词法、语法和语义分析、中间代码生成。
解释方式的优点在于更灵活,交互方便,便于对源程序进行调试和修改。问题是效率较低,加工处理过程的速度较慢:
然而现在,编译器和解释器的界限越来越模糊,Hybrid Implementation Systems
早已兴起,以Java语言的处理器为例:
1.2 编译程序的工作过程
编译程序的主要功能是将源程序翻译成等价的目标程序,这个过程在逻辑上可以分成若干个阶段,每个阶段把源程序从一种表示变换成另一种表示。
1.2.1 词法分析
词法分析是编译过程的基础。任务是扫描源程序,依据语言的词法规则,分析出每个单词(具有独立意义的最小语法单位),并识别出与其相关的属性(是关键字、标识符、界限符、常数、运算符等等),再转换成长度上统一的标准形式(这种统一的标准形式既刻画了单词本身,又刻画了它所具有的属性,称为属性字),以供其它部分使用。
以 position := initial + rate * 60;
为例,词法分析会扫描源程序的字符串,识别单词(关键字、标识符、常量、运算符、界限符):
经过 Scanner
的扫描和分析,得到下面的符号表:
1.2.2 语法分析
语法分析是在词法分析的基础上进行的。任务是依据语言的语法规则,逐一分析词法分析时得到的单词,把单词串分解成各类语法单位(如表达式、语句等),即确定它们是怎样组成说明和语句,以及说明和语句又是怎样组成程序的。通过语法分析,可以确定整个单词符号串是否构成一个语法正确的程序。一旦出现不合语法规则的地方,便将出错的位置及出错性质报告给程序员;如无语法错误,则用另一种中间形式给出正确的语法结构,供下一阶段分析使用。
自然语言,如 <标识符>
是由字母后跟若干个( ≥ 0
)字母或数字的符号串组成。用语法图 Syntax Graph
描绘如下:
Backus-Naur Form
BNF范式用来描述标识符:
<标识符> ::= <字母> | <标识符><字母> | <标识符><数字>
如果是 EBNF: Extended BNF
扩充的BNF范式:
<标识符> ::= <字母> { <字母> | <数字> }
继续以 position := initial + rate * 60;
为例,按照语法规则,分析词法分析时得到的单词,构建一棵抽象语法树:
1.2.3 语义分析
凡在编译时可以确定的内容,称为“静态”的;凡必须推迟到程序运行时才能确定的内容,称为“动态”的。
语义分析的任务是,依据语言的语义规则对语法分析得到的语法结构进行静态语义检查(保证标识符和常数的正确使用、检查类型和运算合法性等),并用另一种内部形式表示出来(符号表或中间代码程序),或者直接用目标语言表示出来。
1.2.4 中间代码生成
中间代码生成对于编译程序来说不是必不可少的阶段。编译程序采用中间代码,并随后对中间代码进行优化,目的是为了最终得到高效率的目标代码。任务是在语法分析和语义分析的基础上,根据语法成分的语义对其进行翻译,翻译的结果即某种中间代码形式。这种中间代码结构简单,接近或者容易翻译成计算机的指令。常用的中间代码形式有三元式(近似于二地址指令)、四元式(近似于三地址指令)和逆波兰式。
举一个例子,与赋值语句 x := a + b * c
对应的四元式如下:
① (*, b, c, t1)
② (+, a, t1, t2)
③ (:=, t2, _, x)
其中,t1, t2
为临时工作变量,_
表示空。
1.2.5 中间代码优化
中间代码优化也不是编译程序的必要阶段。其任务是依据程序的等价变换规则,通过调整和改变中间代码中某些操作的次序,尽量压缩目标程序运行所需的时间和所占的存储空间,以最终产生更加高效的目标代码。注意,优化的是中间代码/目标代码的质量,而非编译程序的质量。
一个更复杂的例子如下:
k = 1;
while (k < 10) {
t1 = 10 * k;
t2 = I + T1;
A = t2;
t3 = 10 * k;
t4 = j + t3;
B = t4;
t5 = k + 1;
k = t5;
}
最终结果如下:
1.2.6 目标代码生成
目标代码生成是编译程序的最后阶段。如果语义分析时,把源程序表示成中间形式而不是表示成目标指令,则由本部分完成从中间形式到目标指令的转换。如果语义分析时,已直接生成目标指令,则无需另做代码生成工作。
目标指令可能是绝对指令代码,或可重新定位的指令代码,或汇编指令代码。该阶段的工作有赖于硬件系统结构和机器指令含义。
1.2.7 表格管理
使用符号表,登记源程序中出现的每个名字以及名字的各种属性。有些名字的属性需要在各个阶段才能填入。
1.2.8 出错处理
源程序中的错误有语法错误和语义错误两种。
- 语法错误:源程序中不符合语法(或词法)规则的错误,它们可在词法分析或语法分析时检测出来。
- 语义错误:源程序中不符合语义规则的错误,一般在语义分析时检测出来,有的语义错误要在运行时才能检测出来。通常包括:说明错误、作用域错误、类型不一致等。
1.3 编译程序的结构
由此可知,一个典型的编译过程包括词法分析、语法分析、语义分析、中间代码生成、中间代码优化和目标代码生成六个阶段,对应的是六个程序模块——词法分析程序、语法分析程序、语义分析程序、中间代码生成程序、中间代码优化程序、目标代码生成程序,此外还包括表格处理程序、出错处理程序:
1.4 编译程序的组织形式
1.4.1 编译的前端和后端
编译过程在逻辑组织上可以分为六个阶段,但实际上往往分为前端和后端。前端主要由依赖于源语言但独立于目标机器的那些部分组成,包括词法分析、语法分析、语义分析、中间代码生成、独立于机器的中间代码优化;后端则由独立于源语言但依赖于中间语言和目标机器(计算机硬件系统和机器指令系统) 的几个部分组成,包括目标代码生成、依赖于机器的代码优化。
把编译过程分成前端和后端两部分。前端只依赖于源程序、独立于目标机器、生成中间代码,后端依赖于目标机器、独立于源语言、和中间语言有关、生成目标代码。这种组织方式的好处在于,提高开发编译器的效率,便于编译程序的移植——如果要移植编译程序到不同类型的机器,只需针对不同的目标机器,修改编译器的后端即可。取一个编译器的前端并重写它的后端,可以得到同一源语言在另一机器上的编译器;取一个编译器的后端,使用不同的前端,从而得到同一机器上的几个编译器(虽然都采用同一中间语言)。
我们可以构建出
n
×
m
n \times m
n×m 个编译器,只使用
n
+
m
n + m
n+m 个组件(
n
n
n 为前端数,
m
m
m 为后端数):
1.4.2 遍(趟,趟程)
编译过程还可以采用分遍的形式,即由一遍或多遍编译程序来完成。所谓一趟或一遍是指一个编译程序在编译时刻,对 源程序或源程序的等价物(中间程序) 从头到尾扫描一遍并完成所规定的工作:
- 在一遍中,可以完成一个或相连几个逻辑步骤的工作。如:词法分析作为第一遍;语法分析和语义分析作为第二遍;代码优化和存储分配作为第三遍;代码生成作为第四遍;从而构成一个四遍扫描的编译程序。
- 或者可将每个逻辑步骤作为一遍或几遍完成,如代码优化分为优化准备和实际代码优化两遍。这种做法适用于存储空间小、要求高质量目标程序的场合。
根据编译程序在完成翻译任务的过程中,需要对源程序或其中间等价物扫描的遍数,可以把编译程序分为:单遍扫描的编译程序(只需扫描一遍),和多遍扫描的编译程序(需扫描多遍)。
并非单遍/多遍一定就好,应视具体情况而定。一个编译程序是否需要分遍、如何分遍,通常根据下面的因素决定:
- 宿主机的存储容量的大小;
- 编译功能的强弱;
- 源语言的繁简;
- 目标程序优化的程度;
- 设计的目的(如:编译速度、目标程序的运行速度)
- 设计和实现编译时参加人员的多少和素质等;
一般来说,当源语言较繁、编译程序功能很强、目标优化程序较高且宿主机存储容量较小时,使用多遍扫描方式。此时每遍的输出结果是下一遍的输入对象。显然,中间代码被加工后不需要继续保留。分遍的特点是:
- 优点:多遍的功能独立、单纯;相互联系简单;逻辑结构清晰;优化准备工作充分,并有利于多人分工合作;节省内存空间;提高目标程序质量;缩短Compiler的开发周期等。
- 缺点:不可避免需要做些重复性工作;延长了编译时间,降低了编译效率。
单遍扫描的编译程序是一种极端情形,并不是每种高级语言都可以用一遍编译程序实现的。这时,整个编译程序同时驻留在内存中,各部分通过调用转接的方式进行连接——当语法分析需要新符号时,就调用词法分析程序;当它识别出某一语法结构时,就调用语义分析程序,对识别出的结构进行语义检查,并调用存储分配、优化和代码生成完成相应的工作。下图中以语法分析程序作为主程序:
1.5 编译程序的自展、移植与自动化
1.5.1 高级语言的自编译特性
构造编译程序可以用机器语言、汇编语言和高级语言。早期使用前两者手工编写编译程序,工作效率极低,而且编出来的编译程序难以阅读,不便维护和移植,优点是运行效率高。后来,使用高级语言作为工具来编写编译程序,节省了时间,且编译程序易于阅读,便于维护和移植。
高级语言的自编译性指一个语言可以用来编写自己的编译程序,也称为自举。毫无疑问,一个具有自编译性的高级语言也可用于编写其他高级语言的编译程序。
例:Pascal语言的编译程序
1.5.2 编译的自展技术
自展技术就是通过一系列自展途径而形成编译程序的过程。对于具有自编译性的高级语言,可运用自展技术构建其编译程序。 一个里程碑的事件是1971年用自展技术生成PASCAL编译程序。
按照自展技术,先把源语言
L
L
L 分解为一个核心部分
L
0
L_0
L0 及其扩充部分
L
1
,
L
2
,
…
,
L
n
L_1, L_2, \dots, L_n
L1,L2,…,Ln ,使得对核心部分
L
0
L_0
L0 进行一次或多次扩充之后得到源语言
L
L
L 。如下图:
分解源语言之后,先用低级语言构造一个小的 L 0 L_0 L0 的编译程序,再以它为工具构造一个能够编译更多语言成分的较大编译程序——用 L 0 L_0 L0 编写 L 1 L_1 L1 的编译程序……用 L i L_i Li 编写 L i + 1 ( i = 1 , 2 , … , n − 1 ) L_{i+1}\ (i = 1, 2, \dots, n - 1) Li+1 (i=1,2,…,n−1) 的编译程序。如此扩展下去,越滚越大,最后形成所期望的整个编译程序 L L L 。可以发现,这一自展过程中,只在 L 0 L_0 L0 的编译程序上使用低级语言,其他编译程序都是用高级语言编写的。
1.5.3 编译的移植
编译程序可以通过移植得到,即将一个机器(宿主机)上的一个具有自编译性的高级语言的编译程序移植到另一个机器(目标机)上。在宿主机 A A A 用高级语言 L L L 编写的源语言为 L L L 的编译程序为 C L A C^{LA} CLA ;在宿主机 A A A 上用机器语言编写的源语言为 L L L 的编译程序为 C O A C^{OA} COA 。
将编译程序 C L A C^{LA} CLA 的前端和后端记为 C F L A , C A L A C^{LA}_F, C^{LA}_A CFLA,CALA ,有: C L A = C F L A + C A L A C^{LA} = C^{LA}_F +C^{LA}_A CLA=CFLA+CALA
为了从宿主机 A A A 上将编译程序 C L A C^{LA} CLA 移植到目标机 B B B 上,要先用源语言 L L L 将 C A L A C^{LA}_A CALA 的后端改为 C A L B C^{LB}_A CALB ,使其能够产生目标机 B B B 的目标代码。改写后,在宿主机 A A A 上产生编译程序: C I L A = C F L A + C A L B C^{LA}_I = C^{LA}_F +C^{LB}_A CILA=CFLA+CALB
然后用 C O A C^{OA} COA 对 C I L A C^{LA}_I CILA 进行编译,生成 C I O A C^{OA}_I CIOA ,这是一个能够在宿主机 A A A 上运行的、能生成目标机 B B B 的目标代码的编译程序。再用这个编译程序 C I O A C^{OA}_I CIOA 对 C I L A C^{LA}_I CILA 进行编译,生成 C O B C^{OB} COB ,这是一个能够在目标机 B B B 上运行、能生成目标机 B B B 的目标代码的编译程序。
交叉编译器(
cross-compiler
)
由于目标机指令系统与宿主机的指令系统不同,编译时将应用程序的源程序在宿主机上生成目标机代码,称为交叉编译。
1.5.4 编译程序的自动化
在编译程序自动化的过程中,开发早、应用广泛的是词法分析程序生成器和语法分析程序生成器。
-
LEX
是比较有代表性的前者,输入正规表达式,输出的是词法分析程序。其基本思想是由正规表达式构造有穷自动机。 -
YACC (Yet Another Compiler Compiler)
是一个基于LALR(1)文法的语法分析程序生成器,接受LALR(1)文法,生成一个相应的LALR(1)分析表和分析器。YACC生成的语法分析器可以和扫描器连接:
注意,LEX和YACC都是关于编译程序前端的生成器,关于编译程序后端的生成器,则出现得晚得多。