编译原理

757 阅读20分钟

第一章

1.1 从面向机器的语言到面向人类的语言

计算机的硬件只能识别由0、1字符串组成的机器指令序列,即机器指令程序。

机器指令程序是最基本的计算机语言

1.1.1高低级语言

以下是面向机器和面向人类语言举例

高级语言特点:实现效率高,执行效率低,对硬件的可控性弱,目标代码大,可维护性好,可移植性好

低级语言特点:实现效率低,执行效率高,对硬件的可控性强,目标代码小,可维护性差,可移植性差

1.2 语言之间的翻译

高级语言之间的翻译,一般被称为转换,如FORTRAN到Ada的转换等,

高级语言可以直接翻译成机器语言,也可以翻译成汇编语言,这两个翻译过程被称为编译

从汇编语言到机器语言的翻译被称为汇编

高级语言是与具体计算机无关的,而汇编语言和机器语言均是与计算机有关的,因此,若将一个汇编语言汇编为可在另一机器上运行的机器指令,则称为交叉汇编,而建立在交叉汇编基础之上的编译模式,如首先将L2编译成A2,再将A2汇编为M1,有时也被称为交叉编译。

上述这些翻译模式一般被认为是正向工程

1.2.1语言之间的翻译模式

1.3 编译器与解释器

编译器

从用户的观点来看,编译器是一个黑盒子

源程序的翻译和翻译后程序的运行是两个独立的不同阶段。

解释器

解释器采用另一种方式翻译源程序。它不像编译器那样,把源程序的翻译和目标程序的运行分割开来,而是把翻译和运行结合在一起进行,翻译一段源程序,紧接着就执行它。

1.3.1编译器与解释器工作方式的对比

解释器与编译器的主要区别在于:运行目标程序时的控制权在解释器而不在目标程序。

解释器有以下特点:

  • 具有较好的动态特性

    运行时,由于源程序也参与其中,因此数据对象的类型可以动态改变,并允许用户对源程序进行修改,且可提供较好的出错诊断,从而为用户提供了交互式的跟踪调试功能。

  • 具有较好的可移植性

    解释器一般也是用某种程序设计语言编写的,因此,只要对解释器进行重新编译,就可以使解释器运行在不同的环境中。

1.4编译器的工作原理与基本组成

1.4.1通用程序设计语言的主要成分

通用程序设计语言的典型特征之一是抽象,其抽象程度是以程序设计语言所支持的基本结构为特征的,可以大致划分为三种形式**:过程**、模块(抽象数据类型,ADT)和

(1)   procedure sample(y: integer);
(2)     var x : integer;
(3)     begin x := y;
(4)            if x>100 then x :=0
(5)    end;

(2)是声明性语句,而(3)~(5)是操作性语句。对于编译器来讲。

它对声明性语句的处理一般是生成相应的环境(存储空间)

而对操作性语句则是生成此环境中的可执行代码序列

为了便于编译器的处理,操作性语句中使用的每个操作对象,均应在使用前进行声明,即遵循先声明后引用的原则。

1.4.2以阶段划分的编译器

编译器对于计算机语言的翻译,也同样需要经历这样几个阶段:

首先进行词法分析,识别出合法的单词;

其次进行语法分析,得到由单词组成的句子结构;

然后进行语义分析,并且生成目标程序。

为了使翻译工作更好地进行,编译器往往在语义分析之后先生成所谓的中间代码,并且可以对中间代码进行优化,最后从优化后的中间代码生成目标程序

1.4.3编译器各阶段的工作

例中每个前一阶段的输出是后一阶段的输入。

一段Pascal源程序语句如下所示

  var x, y, z : real;
   x := y + z * 60;

编译器从左到右扫描输入,首先进行的是词法分析

词法分析器的输入是源程序,输出是识别出的记号流,如图1.4所示。

语法分析器以词法分析器返回的记号流为输入构造句子的结构,并以的形式表示出来,称之为语法树,如图1.5所示。

语义分析器根据语法分析器构造的语法树,进行适当的语义处理

对于声明语句,进行符号表的查填。

下述符号表部分的内容中,每一行存放一个符号的信息,第一行存放标识符x的信息,它的类型是real,为它分配的地址是0。

第二行存放y的信息,它的类型是real,为它分配的地址是4。

由此可知,我们为每个实型数分配一个大小为四个单位的存储空间。

对于可执行语句,检查结构合理的表达式运算是否有意义。

由于变量x,y,z均是real,而60被认为是integer,因此,语义检查时需要进行把60转换为60.0的处理。

反映在语法树上,就是增加了一个新节点itr?(将整型数转换为实型数),如图1.6所示。

由于声明语句并不生成可执行的代码,所以到此为止,对声明语句的处理已经完成。

下边开始的中间代码生成,仅涉及源程序中的赋值句。

中间代码生成器对语法树进行遍历,并生成可以顺序执行的中间代码序列。

最常用的中间代码形式是四元式(三地址码),它的基本形式为:

(op,arg1,arg2,result)

操作符 左操作数 右操作数 结果

上式表示第(序号)个四元式,arg1和arg2进行op运算,结果存进result。

下一步工作就可以对中间代码进行优化了。

分析上边的4个四元式可以看出,60是编译时已经知道的常数,所以把它转换成60.0的工作可以在编译时完成,没有必要生成(1)号四元式。

再看(4)号四元式,它的作用仅是把T3的值传给id1(这样的运算被称为复写传播),不难看出,这条四元式也是多余的。

经过优化后,4个四元式减少为两个,如图1.8所示。

最后根据优化后的中间代码生成目标代码,如图1.9所示。

这里的目标代码是汇编指令,其中MOVF、MULF和ADDF分别表示浮点数的传送、乘和加操作。

1.词法分析

词法分析器根据词法规则识别出源程序中的各个记号(token),每个记号代表一类单词(lexeme)。源程序中常见的记号可以归为以下几大类,其中每一类均可再细分。

(1) 关键字: 如var、begin、end ...,它们在源程序中均有特定含义,一般不作它用,在这种情况下也被称为保留字。

(2) 标识符: 如x、y、z、sort ...,它们在源程序中被用作变量名、过程名、类型名和标号等所有对象的名称。

(3) 字面量: 如60、"Xidian University" ...,一般表示常数或字符串常量,它们也可以被细分为数字字面量、字符串字面量等。

(4) 特殊符号: 如":="、"+"、";" ...,它们在源程序中均有特定含义,根据它们的作用,也可以被细分为运算符、分隔符等。

2.语法分析

语法分析器根据语法规则识别出记号流中的结构(短语、句子等),并构造一棵能够正确反映该结构的语法树

语法树可以是隐含的,也可以确有其“树”。语法树的数据结构一般采用典型的二叉树结构,因为任何形态的树均可以转化为二叉树。

3.语义分析

语义分析器根据语义规则对语法树中的语法单元进行静态语义检查,如类型检查和转换等,其目的在于保证语法正确的结构在语义上也是合法的。

当分析到声明语句时,语义分析器将相应的环境信息记录在符号表中,以便在后继操作语句中使用。

如例1.2中的三个变量都是real类型。而60被默认为integer类型。不同类型的数所占用的存贮空间不同,例如real类型占用4个存贮单元,则三个变量被分配的地址分别为0、4、8。

4.中间代码生成

中间代码生成器根据语义分析器的输出生成中间代码

中间代码可以有若干种形式,它们的共同特征是与具体机器无关

最常用的一种中间代码是三地址码,它的一种实现方式是四元式。

三地址码的优点是便于阅读、便于优化。

无论是对于解释器还是编译器,到中间代码生成以前的各阶段(即完成语义分析)是完全一样的。

语义分析完成以后,语法树已经形成,执行计算的基本元素已经具备,因此,对于解释器来讲,此时就可以直接形成计算步骤并且进行计算,没有必要再做中间代码生成和其后的工作。或者,解释器在语义分析完成以后,生成某种中间代码,统一对此中间代码进行解释执行。

由于语法树和中间代码均不依赖于任何机器,因此解释器是可移植的。典型的例子是Java字节代码与Java虚拟机。

5.中间代码优化

生成的中间代码往往在时间上和空间上有很大浪费。当需要生成高效目标代码时,就必须进行优化。

优化过程可以在中间代码生成阶段进行,也可以在目标代码生成阶段进行。

由于中间代码是不依赖于机器的,在中间代码一级考虑优化可以避开与机器有关的因素,把精力集中在对控制流和数据流的分析上。

因此,优化的大部分工作在目标代码生成之前进行,只有少部分与机器有关的优化(如局部的优化或寄存器的分配等)工作放在目标代码生成时进行。

6.目标代码生成

目标代码生成是编译器的最后一个阶段。

在生成目标代码时要考虑以下几个问题:计算机的系统结构、指令系统、寄存器的分配以及内存的组织等。

编译器生成的目标程序代码可以有多种形式。

(1) 汇编语言形式(Assembly Language Format): 编译器生成汇编语言形式的代码序列。

(2) 可重定位二进制代码形式(Relocatable Binary Format): 这实际上是编译器常采用的一种目标代码。(用相对地址的形式来分配内存等资源)

(3) 内存形式(Memory-Image Format): 编译器生成的代码序列直接被装入原编译器所在的位置并被立即执行,反映在外部也就是编译后马上运行。(一次性)

7.符号表管理

符号表的作用是记录源程序中符号的必要信息,并加以合理组织,从而在编译器的各个阶段能对它们进行快速、准确的查找和操作。符号表中的某些内容甚至要保留到程序的运行阶段。

比如变量的声明,会保存在符号表中。

8.出错处理

用户编写的源程序中往往会有一些错误,这些错误大致被分为静态错误动态错误两类。

所谓动态错误,是指源程序中的逻辑错误,它们发生在程序运行的时候,也被称为动态语义错误,如变量取值为零时被作为除数,数组元素引用时下标出界等。

静态错误又可分为语法错误和静态语义错误

静态错误应该在编译的不同阶段被检查出来,并且采用适当的策略修复它们,使得分析过程能够继续下去,直到源程序的结束。

遇到一个错误就使编译器停止工作的做法是不负责任的,也是用户难以接受的。

1.4.4编译器的分析/综合模式

对于编译器的各个阶段,逻辑上可以把它们划分为两个部分,即分析部分综合部分

从词法分析到中间代码生成各阶段的工作称为分析,而以后直到目标代码生成各阶段的工作被称为综合。

分析部分也被称为编译器的前端,综合部分也被称为编译器的后端。图1.10所示是一个理想的分析/综合模式。

1.4.5编译器扫描的遍数

编译器的每个阶段都是对以某种形式表示的完整程序进行一遍分析。

我们把每个阶段将程序完整分析一遍的工作模式称为一遍扫描

语法分析器进行第二遍扫描,它以词法分析器输出的记号流为输入,识别出语言结构,如赋值语句、过程定义等,并建立和输出对应的语法树。依此类推,最后生成目标程序。

原理上希望扫描的遍数越少越好,这就必须保证两点:

(1) 为编译器的运行提供足够大的空间。由于若干阶段的工作合并在一遍中完成,所以处理各阶段工作的程序都随时准备运行,而且各阶段所需的信息也要同时放在内存中。随着计算机硬件技术的发展,空间已不成为问题。

(2) 在语言的设计上和编译技术上为减少扫描遍数提供支持。在语言设计上,尽量使得编译器可以仅从已扫描过的内容就得到足够的信息。

例如,许多程序设计语言都要求对标识符先声明后引用,这就保证了任何一个标识符出现时就可以确定它的性质,而不需要扫描标识符以后的程序部分。

1.5编译器的编写

编译器本身也是一个程序

第二章

2.1词法分析中的若干问题

2.1.1记号、模式与单词

程序设计语言中,组成语句的基本单元也可根据其在句子中的作用分类。

最基本分类有四类: 

(1) 关键字(保留字).

(2) 标识符:标识符是程序设计语言中最大的一个类别,它的作用是为某个实体起一个名字,以便于今后称呼(引用)可以用标识符来命名的实体包括类型、变量、过程、常量、类、对象、程序包、标号等,即类型名、变量名、过程名、常量名等。

(3) 字面量:字面量是指直接以其字面所表示的常量,如25、true、"This is a string"等。值得注意的是,字面量与常量是两个不同的概念,常量可以是一个字面量(直接表示),也可以是一个常量名(命名表示)。

(4) 特殊符号:程序设计语言中的特殊符号,类似于自然语言中的标点符号,每个符号在程序设计语言中均有特殊用途。可以根据它们的用途,再细分为算符(如+、、*、/等)、分隔符。

显然,一个单词究竟是标识符、关键字,还是特殊符号,需要根据一定的构词规则来产生和识别。我们将产生和识别单词的规则称为模式(patten),按照某个模式(规则)识别出的元素称为记号(token),而单词(lexeme)一词是指被识别出元素自身的值。

【例2.1】 对于语句:position := initial + rate * 60,可以识别出下述序列:

标识符 特殊符号 标识符 特殊符号 标识符 特殊符号 数字字面量

其中position、initial、rate均被识别为标识符,因为它们均符合同一条规则,即以字母打头的字母数字串。

记号至少含有两个信息:一个是记号的类别(记号有四种类别);另一个是记号的值

第一列的01 03 81等是序号

comment表示注释,它们的特点是一个记号类别可以对应若干个单词。由于语法分析及其后的阶段并不对注释进行分析,因而可在词法分析阶段中滤掉注释,即词法分析器可以不向语法分析器返回comment。

2.1.2记号的属性

记号至少包含两个部分:记号类别和记号的其他信息。

例如所有的关系运算符均可以由relation来标识,而所有字符串字面量均可以由literal来标识。

记号的类别可以用整型编码或枚举类型表示

【例2.2】 表达式mycount > 25由表2.2的三个记号组成。其中标识符的属性值也可以由mycount在符号表中的入口(下标)来表示。

2.1.3词法分析器的作用与工作方式

词法分析器是编译器中唯一与源程序打交道的部分,从某种意义说,也可以被认为是整个编译器的预处理器。它的主要工作包括:

(1) 掉源程序中的无用成分,如注释、空格、回车等。

(2) 处理与具体平台有关的输入。不同的操作系统或相关软件构成的平台,对某些特殊符号(如文件结束符等)可能有不同表示,因此需要在词法分析阶段分情况处理。

(3) 识别记号,并交给语法分析器。这是词法分析器的主要任务,本章将在各节中详细讨论。(按照模式)

(4) 调用符号表管理器或出错处理器,进行相关处理。词法错误是源程序中常见的错误。值得注意的是,词法错误往往不是由词法分析器检查出来的,而是由语法分析器发现的。这是因为,源程序中除了非法字符之外的大部分字符或字符串,都可以被词法分析器的某个模式所匹配,从而被识别成一个记号。而这些记号的正确与否,在没有上下文对照的情况下,是很难判断的。

根据编译器的总体需求,词法分析器在整个编译器中可以有不同的工作方式。

(1) 词法分析器作为语法分析器的子程序。其工作方式如图2.1所示。

(2) 词法分析器进行单独的一遍扫描。其工作方式如图2.2所示。

(3) 与语法分析器并行工作的模式。上述两种词法分析器的工作模式与语法分析器的关系均被认为是串行的。

为了提高编译器的效率,可以通过一个队列,使词法分析器和语法分析器以生产/消费的形式并行工作。

词法分析器将识别出的记号流输出到队列中,语法分析器从队列中取得记号,只要队列中有识别出的记号,则词法分析器和语法分析器就可以同时工作。其工作方式如图2.3所示。

2.1.4 输入缓冲区

缓冲区出现在在高速设备和低速设备之间,弥补速度差

词法分析器是编译器中读入源程序字符序列的唯一阶段,而很长的编译时间又消耗在词法分析阶段,所以,加快词法分析是设计编译器时要考虑的重要问题之一。可以通过设立输入缓冲区来加快读入源程序字符序列的速度

输入缓冲区一般被设计为一块与磁盘扇区大小成倍数关系的内存。若一个扇区为1024字节,则输入缓冲区可以取1024、4096或8192字节等。这样可以保证对缓冲区的一次输入所需的I/O操作次数尽可能少。

输入缓冲区的安排一般采用单缓冲区或双缓冲区(缓冲区对)的方式。下边所介绍的是单缓冲区方式,它也是词法分析器生成器FLEX所采用的方式。

图2.4是一个单缓冲区的示意图。有效输入序列从缓冲区的起始位置开始存放,最后添加一个特殊标记(此处用#表示):若缓冲区一次装不下整个源程序,它就表示缓冲区的结束,否则它紧跟在文件结束符(eof)之后,表示整个输入源程序的结束。

用两个指针c_ptr和f_ptr分别指向当前被识别记号的第一个字符和向前扫描的字符。最初,两个指针同时指向下一个被识别记号的第一个字符,f_ptr向前扫描,直到某个模式匹配成功。一旦这个记号被确定,f_ptr指向被识别出记号的右端字符,在此记号被处理后,两个指针都移向该记号之后的下一个字符。

2.2 模式的形式化描述

2.2.1 字符串与语言

从词法分析的角度看,程序设计语言是由记号组成的集合,每个记号又是由若干字母按照一定规则组成的字符串。

在下述的讨论中,我们首先定义一个泛泛的“语言”,然后在此基础上规定一个正规集,而程序设计语言就是一个正规集。

定义2.1 语言L是有限字母表∑上有限长度字符串的集合。

定义2.1明确指出,语言是一个集合,集合中的元素是字符串,并且强调了两个有限:

(1) 字母表是有限的,即字母表中元素是有限多个; (2) 字符串的长度是有限的,即字符串中字符个数是有限多个。

这是由于计算机所能表示的字符个数和字符串的长度都是有限的。

2.2.2 正规式与正规集