编译原理随笔
1.2 编译器的结构
分析(analysis)
综合(synthesis)
一个编译器的各个步骤
1.2.1 词法分析
编译器的第一个步骤称为词法分析(lexical analysis)或扫描( scanning)。词法分析器读人组成源程序的字符流,并且将它们组织成为有意义的词素(lexeme)的序列。
1.2.2 语法分析
语法分析器使用由词法分析器生成的各个词法单元的第-一个分量来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元流的语法结构。一个常用的表示方法是语法树( syntax tree), 树中的每个内部结点表示一个运算,而该结点的子结点表示该运算的分量。在图1-7中,词法单元流(1.2)对应的语法树被显示为语法分析器的输出。
1.2.3 语义分析
语义分析器(semanticanalyzer)使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一-致。它同时也收集类型信息,并把这些信息存放在语法树或符号表中,以便在随后的中间代码生成过程中使用。
1.2.4 中间代码的生成
在把一个源程序翻译成目标代码的过程中,一个编译器可能构造出一个或多个中间表示。这些中间表示可以有多种形式。语法树是一种中间表示形式,它们通常在语法分析和语义分析中使用。
1.2.5 代码优化
机器无关的代码优化步骤试图改进中间代码,以便生成更好的目标代码。“更好”通常意味着更快,但是也可能会有其他目标,如更短的或能耗更低的目标代码。
1.2.6 代码生成
代码生成器以源程序的中间表示形式作为输人,并把它映射到目标语言。如果目标语言是机器代码,那么就必须为程序使用的每个变量选择寄存器或内存位置。然后,中间指令被翻译成为能够完成相同任务的机器指令序列。代码生成的一个至关重要的方面是合理分配寄存器以存放变量的值。
1.2.7符号表管理
编译器的重要功能之--是记录源程序中使用的变量的名字,并收集和每个名字的各种属性有关的信息。这些属性可以提供-一个名字的存储分配、它的类型、作用域(即在程序的哪些地方可以使用这个名字的值)等信息。对于过程名字,这些信息还包括:它的参数数量和类型、每个参数的传递方法(比如传值或传引用)以及返回类型。
1.2. 8 将多个步骤组合成趟
前面关于步骤的讨论讲的是-一个编译器的逻辑组织方式。在-一个特定的实现中,多个步骤的活动可以被组合成一趟( pass)。每趟读人一个输人文件并产生-一个输出文件。比如,前端步骤中的词法分析、语法分析、语义分析,以及中间代码生成可以被组合在一.起成为- - 趟。代码优化可以作为一个可选的趟。然后可以有一个为特定目标机生成代码的后端趟。
1.2.9 编译器构造工具
这些工具使用专用的语言来描述和实现特定的组件,其中的很多工具使用了相当复杂的算法。其中最成功的工具都能够隐藏生成算法的细节,并且它们生成的组件易于和编译器的其他部分相集成。
一些常用的编译器构造工具包括:
1)语法分析器的生成器:可以根据一个程序设计语言的语法描述自动生成语法分析器。
2)扫描器的生成器:可以根据一个语言的语法单元的正则表达式描述生成词法分析器。
3)语法制导的翻译引擎:可以生成一组用于遍历分析树并生成中间代码的例程。
4)代码生成器的生成器:依据一组关于如何把中间语言的每个运算翻译成为目标机上的机器语言的规则,生成一个代码生成器。
5)数据流分析引擎:可以帮助收集数据流信息,即程序中的值如何从程序的一个部分传递到另一部分。 数据流分析是代码优化的-一个重要部分。
6)编译器构造工具集:提供了可用于构造编译器的不同阶段的例程的完整集合。
1.3 程序设计语言的发展历程
第一台电子计算机出现在20世纪40年代。它使用由0、1序列组成的机器语言编程,这个序列明确地告诉计算机以什么样的顺序执行哪些运算。运算本身也是很低层的:把数据从一个位置移动到另一个位置,把两个寄存器中的值相加,比较两个值,等等。不用说,这种编程速度慢且枯燥,而且容易出错。写出的程序也是难以理解和修改的。
1.3. 1 走向高级程序设计语言
当前有几千种程序设计语言。可以通过不同的方式对这些语言进行分类。方式之一是通过语言的代来分类。第一代语言是机器语言,第二代语言是汇编语言,而第三代语言是Fortran、Cobol、Lisp、C、C++、C#及Java这样的高级程序设计语言。第四代语言是为特定应用设计的语言,比如用于生成报告的NOMAD,用于数据库查询的SQL和用于文本排版的Postscript。术语第五代语言指的是基于逻辑和约束的语言,比如Prolog和OPS5。
1.3.2 对编译器的影响
40年代以来,计算机体系结构也有了很大的发展。编译器的设计者不仅需要跟踪新的语言特征,还需要设计出新的翻译算法,以便尽可能地利用新硬件的能力。
1.4 构建一个编译器的相关科学
编译器的设计中有很多通过数学方法抽象出问题本质从而解决现实世界中复杂问题的完美例子。这些例子可以被用来说明如何使用抽象方法来解决问题:接受一个问题,写出抓住了问题的关键特性的数学抽象表示,并用数学技术来解决它。
问题的表达必须根植于对计算机程序特性的深入理解,而解决方法必须使用经验来验证和精化。
1.4.1 编译器设计和实现中的建模
对编译器的研究主要是有关如何设计正确的数学模型和选择正确算法的研究。设计和选择时,还需要考虑到对通用性及功能的要求与简单性及有效性之间的平衡。最基本的数学模型是我们将在第3章介绍的有穷状态自动机和正则表达式。这些模型可以用于描述程序的词法单位(关键字、标识符等)以及描述被编译器用来识别这些单位的算法。最基本的模型中还包括上下文无关文法,它用于描述程序设计语言的语法结构,比如嵌套的括号和控制结构。我们将在第4章研究文法。类似地,树形结构是表示程序结构以及程序到目标代码的翻译方法的重要模型。我们将在第5章介绍这一概念。
1.4.2 代码优化的科学
在编译器设计中,术语“优化”是指编译器为了生成比浅显直观的代码更加高效的代码而做的工作。“优化”这个词并不恰当,因为没有办法保证--个编译器生成的代码比完成相同任务的任何其他代码更快,或至少一样快。
1.5 编译技术的应用
编译器设计并不只是关于编译器的。很多人用到了在学校里研究编译器时学到的技术,但是严格地说,它们从没有为一个主流的程序设计语言编写过一个编译器(甚至其中的一部分)。编译器技术还有其他重要用途。另外,编译器设计影响了计算机科学中的其他领域。在本节,我们将回顾和编译技术有关的最重要的互动和应用。
1.5.1 高级程序设计语言的实现
一个高级程序设计语言定义了一个编程抽象:程序员使用这个语言表达算法,而编译器必须把这个程序翻译成目标语言。总的来说,用高级程序设计语言编程比较容易,但是比较低效,也就是说,目标程序运行较慢。使用低级程序设计语言的程序员能够更多地控制-一个计算过程,因此从原则上讲,可以产生更加高效的代码。遗憾的是,低级程序比较难编写,而且更糟糕的是可移植性较差,更容易出错,而且更加难以维护。优化编译器包括了提高所生成代码性能的技术,因此弥补了因高层次抽象而引人的低效率。
1.5.2 针对计算机体系结构的优化
计算机体系结构的快速发展也对新编译器技术提出了越来越多的需求。几乎所有的高性能系统都利用了两种技术:并行( parallelism)和内存层次结构( memory hierarchy)。并行可以出现在多个层次上:在指令层次上,多个运算可以被同时执行;在处理器层次上,同一个应用的多个不同线程在不同的处理器上运行。内存层次结构是应对下述局限性的方法:我们可以制造非常快的内存,或者非常大的内存,但是无法制造非常大又非常快的内存。
并行性
所有的现代微处理器都采用了指令级并行性。但是,这种并行性可以对程序员隐藏起来。程序员写程序的时候就好像所有指令都是顺序执行的。
内存层次结构
一个内存层次结构由几层具有不同速度和大小的存储器组成。离处理器最近的层速度最快但是容量最小。如果一个程序的大部分内存访问都能够由层次结构中最快的层满足,那么程序的平均内存访问时间就会降低。并行性和内存层次结构的存在都会提高一个机器的潜在性能。但是,它们必须被编译器有效利用才能够真正为一个应用提供高性能计算。
1.5.3 新计算机体系结构的设计
编译器是在机器建造好之后再开发的。现在,这种情况已经有所改变。因为使用高级程序设计语言是一种规范,决定一个计算机系统性能的不是它的原始速度,还包括编译器能够以何种程度利用其特征。因此,在现代计算机体系结构的开发中,编译器在处理器设计阶段就进行开发,然后编译得到代码并运行于模拟器上。这些代码被用来评价提议的体系结构特征。
RISC
有关编译器如何影响计算机体系结构设计的最有名的例子之一是 RISC( Reduced Instruction-Set Computer,精简指令集计算机)的发明。在发明RISC之前,趋势是开发的指令集越来越复杂,以使得汇编编程变得更容易。这些体系结构称为CISC( Complex Instruction-Set Computer,复杂指令集计算机)。比如,CISC指令集包含了复杂的内存寻址模式来支持对数据结构的访问,还包含了过程调用指令来保存寄存器和向栈中传递参数。编译器优化经常能够消除复杂指令之间的冗余,把这些指令削减为少量较简单的运算。因此,人们期望设计出简单指令集。编译器可以有效地使用它们,而硬件也更容易进行优化。大部分通用处理器体系结构,包括PowerPC、SPARC、MIPS、 Alpha和PA-RISC,都是基于RISC概念的。虽然x86体系结构(最流行的微处理器)具有CISC指令集,但在这个处理器本身的实现中使用了很多为RISC机器发展得到的思想。不仅如此,使用高性能x86机器的最有效的方法是仅使用它的简单指令。
专用体系结构
在过去的30年中,提出了很多的体系结构概念。其中包括:数据流机器、向量机、VLIW(非常长指令字)机器、SIMD(单指令,多数据)处理器阵列、心动阵列( systolie aray)、共享内存的多处理器、分布式内存的多处理器。每种体系结构概念的发展都伴随着相应编译器技术的研究和发展。
1.5.4 程序翻译
我们通常把编译看作是从一个高级语言到机器语言的翻译过程。同样的技术也可以应用到不同种类的语言之间的翻译。下面是程序翻译技术的一些重要应用。
二进制翻译
编译器技术可以用于把一个机器的二进制代码翻译成另一个机器的二进制代码,使得可以在一个机器上运行原本为另一个指令集编译的程序。x86在个人计算机市场.上的主导地位,很多软件都是以x86二进制代码的形式提供的。人们开发了二进制代码翻译器,把x86代码转换成Alpha和Sparc的代码。二进制翻译也可以被用来提供向后兼容性。
硬件合成
硬件设计通常是在寄存器传输层( Register Transfer Level,RTL)_上描述的。在这个层中,变量代表寄存器,而表达式代表组合逻辑。硬件合成工具把RTL描述自动翻译成为门电路,而门电路再被翻译成为晶体管,最后生成一一个物理布局。和程序设计语言的编译器不同,这些工具经常会花费几个小时来优化门电路。还存在一些用来翻译更高层次(比如行为和函数层次)的设计描述的技术。
数据查询解释器
除了描述软件和硬件,语言在很多应用中都是有用的。比如,查询语言(特别是SQL语言( Structured Query Language,结构化查询语言)被用来搜索数据库。数据库查询由包含了关系和布尔运算符的断言组成。它们可以被解释,也可以编译为代码,以便在-一个数据库中搜索满足这个断言的记录。
编译然后模拟
模拟是在很多科学和工程领域内使用的通用技术。它用来理解一个 现象或者验证一个设计。
1.5.5 软件生产率工具
程序可以说是人类迄今为止生产出的最复杂的工程制品,它们包含了很多很多的细节。要使得程序能够完全正确运行,每个细节都必须是正确的。结果是程序中的错误很是猖獗。错误可以使一个系统崩溃,产生错误的输出,使得系统容易受到安全性攻击,在关键系统中甚至会引起灾难性的运行错误。测试是对系统中的错误进行定位的主要技术。
一个很有意思且很有前景的辅助性方法是通过数据流分析技术静态地(即在程序运行之前)定位错误。数据流分析可以在所有可能的执行路径上找到错误,而不是像程序测试的时候所做的那样,仅仅是在那些由输人数据组合执行的路径上找错误。很多原本为编译器优化所开发的数据流分析技术可以用来创建相应的工具,帮助程序员完成他们的软件工程任务。
类型检查
边界检查
很多系统中的安全漏洞都是因为用C语言编写的程序中的缓冲区溢出造成的。因为C语言没有数组边界检查,所以必须由用户来保证对数组的访问没有超出边界。因为不能检验用户提供的数据是否可能溢出一个缓冲区,程序可能被欺骗,把一个数据存放到缓冲区之外。攻击者可以巧妙处理这些数据,使得程序做出错误的行为,从而危及系统的安全。
内存管理工具
垃圾收集机制是在效率和易编程及软件可靠性之间进行折衷处理的另一个极好的例子。自动的内存管理消除了所有的内存管理错误(比如内存泄漏)。这些错误是C或C++程序中问题的主要来源之一。人们开发了很多工具来帮助程序员寻找内存管理错误。比如,Purify 是一个能够动态地捕捉内存管理错误的被广泛使用的工具。还有一些能够帮助静态识别部分此类错误的工具也已经被开发出来。
1.6程序设计语言基础
1.6.1 静态和动态的区别
如果一个语言使用的策略支持编译器静态决定某个问题,那么我们说这个语言使用了一个静态(static)策略,或者说这个问题可以在编译时刻(compiletime)决定。另一方面,一个只允许在运行程序的时候做出决定的策略被称为动态策略(dynamicpolicy),或者被认为需要在运行时刻( run time)做出决定。
我们需要注意的另一个问题是声明的作用域。x的一个声明的作用域(scope)是指程序的一个区域,在其中对x的使用都指向这个声明。如果仅通过阅读程序就可以确定一个声明的作用域,那么这个语言使用的是静态作用域(static scope),或者说词法作用域(lexical scope)。否则,这个语言使用的是动态作用域( dynamic scope)。如果使用动态作用域,当程序运行时,同一个对x的使用会指向x的几个声明中的某一个。大部分语言(比如C和Java)使用静态作用域。
1.6.2环境与状态
1)环境(environment)是一个从名字到存储位置的映射。因为变量就是指内存位置(即C语言中的术语“左值”),我们还可以换一种方法,把环境定义为从名字到变量的映射。
2)状态( state)是一个从内存位置到它们的值的映射。以C语言的术语来说,即状态把左值映射为它们的相应右值。
1.6.3 静态作用域和块结构
1.6.4显式访问控制
1.6.5 动态作用域
例1.7
动态作用域解析对多态过程是必不可少的。所谓多态过程是指对于同一个名字根据参数
类型具有两个或多个定义的过程。
例1.8
面向对象语言的一个突出特征就是每个对象能够对一个消息做出适当反应,调用相应
的方法。换句话说,执行x.m()时调用哪个过程要由当时x所指向的对象的类来决定。一个典型
的例子如下:
1)有一个类C,它有一个名字为m( )的方法。
2) D是C的一个子类,而D有一个它自己的名字为m( )的方法。
3)有一个形如x. m( )的对x的使用,其中x是类C的一个对象。
正常情况下,在编译时刻不可能指出x指向的是类C的对象还是其子类D的对象。如果这
个方法被多次应用,那么很可能某些调用作用在由x指向的类C的对象,而不是类D的对象,而
其他调用作用于类D的对象之上。只有到了运行时刻才可能决定应当调用m的哪个定义。因
此,编译器生成的代码必须决定对象x的类,并调用其中的某一个名字为m的方法。
1.6.6 参数传递机制
值调用
引用调用
1.6.7 别名
引用调用或者其他类似的方法,比如像Java中那样把对象的引用当作值传递,会引起一个有
趣的结果。有可能两个形式参数指向同一个位置,这样的变量称为另一个变量的别名( alias)。
结果是,任意两个看起来从两个不同的形式参数中获得值的变量也可能变成对方的别名。
1. 7 第1章总结
●语言处理器:一个集成的软件开发环境,其中包括很多种类的语言处理器,比如编译器、解释器、汇编器、连接器、加载器、调试器以及程序概要提取工具。
●编译器的步骤:一个编译器的运作需要一 系列的步骤,每个步骤把源程序从一个中间表、示转换成为另一个中间表示。
●机器语言和汇编语言:机器语言是第一代程序设计语言,然后是汇编语言。使用这些语言进行编程既费时,又容易出错。
●编译器设计中的建模:编译器设计是理论对实践有很大影响的领域之-一。已知在编译器设计中有用的模型包括自动机、文法、正则表达式、树型结构和很多其他理论概念。
●代码优化:虽然代码不能真正达到最优化,但提高代码效率的科学既复杂又非常重要。它是编译技术研究的一个主要部分。
●高级语言:随着时间的流逝,程序设计语言担负了越来越多的原先由程序员负责的任务,比如内存管理、类型一致性检 查或代码的并发执行。
●编译器和计算机体系结构:编译器技术影响了计算机的体系结构,同时也受到体系结构发展的影响。体系结构中的很多现代创新都依赖于编译器能够从源程序中抽取出有效利用硬件能力的机会。
●软件生产率和软件安全性:使得编译器能够优化代码的技术同样能够用于多种不同的程序分析任务。这些任务既包括探测常见的程序错误,也包括发现程序可能会受到已被黑客们发现的多种人侵方式之一的伤害。
●作用域规则:一个x的声明的作用域是一段上下文,在此上下文中对x的使用指向这个声明。如果仅仅通过阅读某个语言的程序就可以确定其作用域,那么这个语言就使用了静态作用域,或者说词法作用域。否则这个语言就使用了动态作用域。
●环境:名字和内存位置关联,然后再和值相关联。这个情况可以使用环境和状态来描述。其中环境把名字映射成为存储位置,而状态则把位置映射到它的值。
●块结构:允许语句块相互嵌套的语言称为块结构的语言。假设一个块中有一个x的声明D,而嵌套于这个块中的块B中有-一个对名字x的使用。如果在这两个块之间没有其他声明了x的块,那么这个x的使用位于D的作用域内。
●参数传递:参数可以通过值或引用的方式从调用过程传递给被调用过程。当通过值传递方式传递大型对象时,实际被传递的值是指向这些对象本身的引用。这样就变成了一个高效的引用调用。
●别名:当参数被以引用传递方式(高效地)传递时,两个形式参数可能会指向同一个对象。这会造成一-个变量的修改改变了另一个变量的值。
第2章一个简单的语法制导翻译器
开发一个可运行的Java程序来演示这些编译技术。可以将具有代表性的程序设计语言语句翻译为三地址代码(一种中间表示形式)。重点是编译器的前端,特别是词法分析、语法分析和中间代码生成。在第7章和第8章将介绍如何根据三地址代码生成机器指令。
首先建立一个能够将中缀算术表达式转换为后缀表达式的语法制导翻译器。然后我们将扩展这个翻译器,使它能将某些程序片段(如图2-1所示)转换为如图2-2所示的三地址代码。