1.1 语言处理器
- 编译器和解释器的区别
编译器是把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;而解释器则是只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的。
- Java语言处理器结合了编译和解释过程
一个Java程序先被编译成字节码(bytecode)的中间表示形式,然后由虚拟机对字节码加以解释执行
- 语言处理器的组成
1个语言处理器包含:预处理器->编译器->汇编器->链接器/加载器
1.2 一个编译器的结构
- 编译器把源程序映射为等价的目标程序,这个映射过程由两个部分组成:分析部分和综合部分
- 分析(analysis)部分把源程序分解成为多个组成要素,并在这些要素之上加上语法结构。然后,它使用这个结构来创建该源程序的一个中间表示。如果分析部分检查出源程序没有按照正确的语法构成,或者语义上不一致,它就必须提供有用的信息,使得用户可以按此进行改正。分析部分还会收集有关源程序的信息,并把信息存放在一个称为符号表(symbol table)的数据结构中。符号表将和中间表示形式一起传送给综合部分;
- 综合(synthesis)部分根据中间表示和符号表中的信息来构造用户期待的目标程序。
分析部分经常被称为编译器的前端(front end),而综合部分称为后端(back end)
- 编译器包含的各个步骤:词法分析->语法分析->语义分析->中间代码生成器->机器无关代码优化器->代码生成器->机器代码相关优化器
1.2.1 词法分析
- 词法分析器读入组成源程序的字符流,并且将它们组织成为有意义的词素的序列
- 一个赋值语句的翻译
1.2.2 语法分析
- 语法分析器
语法分析器使用由词法分析器生成的各个词法单元的第一个分量来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元流的语法结构。一个常用的表示方法是语法树(syntax tree),树中的每个内部结点表示一个运算,而该结点的子结点表示该运算的分量。
1.2.3 语义分析
- 语义分析器
语义分析器(semantic analyzer)使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。它同时也收集类型信息,并把这些信息存放在语法树或符号表中,以便在随后的中间代码生成过程中使用。
- 类型检查
语义分析的一个重要部分是类型检查(type checking)。编译器检查每个运算符是否具有匹配的运算分量
- 自动类型转换
程序设计语言可能允许某些类型转换,这被称为自动类型转换(coercion)。比如,一个二元算术运算符可以应用于一对整数或者一对浮点数。如果这个运算符应用于一个浮点数和一个整数,那么编译器可以把该整数转换(或者说自动类型转换)成为一个浮点数。
1.2.4 中间代码生成
- 概念解释
在源程序的语法分析和语义分析完成之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。我们可以把这个表示看作是某个抽象机器的程序。该中间表示应该具有两个重要的性质:它应该易于生成,且能够被轻松地翻译为目标机器上的语言。
- 三地址代码
三地址代码(three-address code)是一种中间表示形式。这种中间表示由一组类似于汇编语言的指令组成,每个指令具有三个运算分量。每个运算分量都像一个寄存器。
- 三地址代码的3个要点
- 首先,每个三地址赋值指令的右部最多只有一个运算符。因此这些指令确定了运算完成的顺序。比如乘法应该在加法之前完成。
- 第二,编译器应该生成一个临时名字以存放一个三地址指令计算得到的值。
- 第三,有些三地址指令的运算分量的少于三个(比如上面的序列1.3中的第一个和最后一个指令)。
1.2.5 代码优化
- 概念解释
机器无关的代码优化步骤试图改进中间代码,以便生成更好的目标代码。“更好”通常意味着更快,但是也可能会有其他目标,如更短的或能耗更低的目标代码。
- 例子优化步骤
- 把60从整数转换为浮点数的运算可以在编译时刻一劳永逸地完成。因此,用浮点数60.0来替代整数60就可以消除相应的inttofloat运算。
- t3仅被使用一次,用来把它的值传递给id1。
1.2.6 代码生成
- 概念解释
代码生成器以源程序的中间表示形式作为输入,并把它映射到目标语言。如果目标语言是机器代码,那么就必须为程序使用的每个变量选择寄存器或内存位置。然后,中间指令被翻译成为能够完成相同任务的机器指令序列。代码生成的一个至关重要的方面是合理分配寄存器以存放变量的值。
- 例子代码生成步骤
- LDF R2, ID3 ,把地址id3中的内容加载到寄存器R2中
- MUL R2, R2 ,#60.0:将R2与浮点常数60.0相乘。井号“#”表示60.0应该作为一个立即数处理。
- LDR R1, id2 ,把id2移动到寄存器R1中
- ADDF R1, R1, R2 ,把前面计算得到并存放在R2中的值加到R1上。
- STF id1,R1 , 最后,在寄存器R1中的值被存放到id1的地址中去。
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 走向高级程序设计语言
- 汇编语言
走向更加人类友好的程序设计语言的第一步是20世纪50年代早期人们对助记汇编语言的开发。一开始,一个汇编语言中的指令仅仅是机器指令的助记表示。后来,宏指令被加入到汇编语言中。这样,程序员就可以通过宏指令为频繁使用的机器指令序列定义带有参数的缩写。
- 高级程序设计语言
走向高级程序设计语言的重大一步发生在20世纪50年代的后五年。其间,用于科学计算的Fortran语言,用于商业数据处理的Cobol语言和用于符号计算的Lisp语言被开发出来。在这些语言的基本原理是设计高层次表示方法,使得程序员可以更加容易地写出数值计算、商业应用和符号处理程序。
- 语言分类方式一
-
- 第一代语言是机器语言
- 第二代语言是汇编语言
- 第三代语言是Fortran、Cobol、Lisp、C、C++、C#及Java这样的高级程序设计语言。
- 第四代语言是为特定应用设计的语言,比如用于生成报告的NOMAD,用于数据库查询的SQL和用于文本排版的Postscript。
- 第五代语言指的是基于逻辑和约束的语言,比如Prolog和OPS5。
- 语言分类方式二
-
- 强制式(imperative)语言:把程序中指明如何完成一个计算任务的语言的称为强制式(imperative)语言,诸如C、C++、C#和 Java等语言都是强制式语言。所有强制式语言中都有用于表示程序状态和语句的表示方法,。
- 声明式(declarative)语言:把程序中指明要进行哪些计算的语言称为声明式(declarative)语言。像ML、Haskell这样的函数式语言和Prolog这样的约束逻辑语言通常被认为是声明式语言。
- 术语
- 冯·诺伊曼语言(von Neumann language)是指以冯·诺伊曼计算机体系结构为计算模型的程序设计语言。今天的很多语言(比如Fortran和C)都是冯·诺伊曼语言。
- 面向对象语言(object-oriented language)指的是支持面向对象编程的语言,面向对象编程是指用一组相互作用的对象组成程序的编程风格。Simula 67 和Smalltalk是早期的主流面向对象语言。C++、C#、Java和Ruby是现在常用的面向对象语言。
- 脚本语言(scripting language)是具有高层次运算符的解释型语言,它通常被用于把多个计算过程“粘合”在一起。这些计算过程被称为脚本。Awk、JavaScript、Perl、PHP、Python、Ruby和Tcl是常见的脚本语言。使用脚本语言编写的程序通常要比用其他语言(比如C)写的等价的程序短很多。
1.3.2 对编译器的影响
- 程序设计语言的设计和编译器是紧密相关的,程序设计语言的发展向编译器的设计者提出了新的要求。他们必须设计相应的算法和表示方式来翻译和支持新的语言特征。
- 从20世纪40年代以来,计算机体系结构也有了很大的发展。编译器的设计者不仅需要跟踪新的语言特征,还需要设计出新的翻译算法,以便尽可能地利用新硬件的能力。
- 通过降低用高级语言程序的执行开销,编译器还可以推动这些高级语言的使用。
- 要使得高性能计算机体系结构能够高效运行用户应用,编译器也是至关重要的。实际上,计算机系统的性能是非常依赖于编译技术的,以至于在构建一个计算机之前,编译器会被用作评价一个体系结构概念的工具。
- 编写编译器是很有挑战性的。编译器本身就是一个大程序。而且,很多现代语言处理系统在同一个框架内处理多种源语言和目标机。也就是说,这些系统可以被当做一组编译器来使用,可能包含几百万行代码。因此,好的软件工程技术对于创建和发展现代的语言处理器是非常重要的。
- 编译器必须能够正确翻译用源语言书写的所有程序。这样的程序的集合通常是无穷的。为一个源程序生成最佳目标代码的问题一般来说是不可判定的。因此,编译器的设计者必须作出折衷处理,确定解决哪些问题,使用哪些启发式信息,以便解决高效代码生成的问题。
1.3.3 1.3节的练习答案:
强制式的:C,C++
声明式的:Lisp,ML
冯诺依曼式的:Fortran,C
面向对象的:C++,Java,Python,VB
函数式的:ML
第三代:Fortran,Cobol,Lisp,C,C++,Java
第四代(为特定应用设计的语言):ML Cobol(数据处理)
脚本语言:Python,Perl
1.4 构建一个编译器的相关科学
1.4.1 编译设计和实践中的建模
- 编译器的研究内容
对编译器的研究主要是有关如何设计正确的数学模型和选择正确算法的研究。设计和选择时,还需要考虑到对通用性及功能的要求与简单性及有效性之间的平衡。
- 数学模型
- 有穷状态自动机:用于描述程序的词法单位(关键字、标识符等)
- 正则表达式:描述被编译器用来识别这些单位的算法。
- 上下文无关文法,它用于描述程序设计语言的语法结构,比如嵌套的括号和控制结构。
- 树形结构:表示程序结构以及程序到目标代码的
1.4.2 代码优化中的科学
- 概念
术语“优化”是指编译器为了生成比浅显直观的代码更加高效的代码而做的工作
- 代码优化因为处理器体系结构变得更加复变,因为巨型并发计算机要求实质性的优化得更加重要
- 编译器优化必须满足下面的设计目标:
-
- 优化必须是正确的,也就是说,不能改变被编译程序的含义。
- 优化必须能够改善很多程序的性能。
- 优化所需的时间必须保持在合理的范围内。
- 所需要的工程方面的工作必须是可管理的。
1.5 编译技术的应用
1.5.1 高级程序设计语言的实现
- 优化编译器包括了提高所生成代码性能的技术,因此弥补了因高层次抽象而引入的低效率。
- 数据流优化,它可以对程序的数据流进行分析,并消除这些构造之间的冗余。
- 面向对象存在大量函数,编译器优化技术必须能够很好地跨越源程序中的过程边界进行优化。例如过程内联和加速虚拟方法分发的优化技术。
- 对于Java等高级语言的很多特性支持:垃圾收集器,可移植
1.5.2 针对计算机体系结构的优化
- 高性能系统都利用的两种技术:
-
- 并行(parallelism):可以出现在多个层次上:在指令层次上,多个运算可以被同时执行;在处理器层次上,同一个应用的多个不同线程在不同的处理器上运行。
- 内存层次结构是应对下述局限性的方法:我们可以制造非常快的内存,或者非常大的内存,但是无法制造非常大又非常快的内存。
1.5.3 新计算机体系结构的设计
- CISC:
在发明RISC之前,趋势是开发的指令集越来越复杂,以使得汇编编程变得更容易。CISC(Complex Instruction-Set Computer,复杂指令集计算机)。
- RISC
编译器优化经常能够消除复杂指令之间的冗余,把这些指令削减为少量较简单的运算。因此,人们期望设计出简单指令集。编译器可以有效地使用它们,而硬件也更容易进行优化。
- 专用体系结构
很多的体系结构概念。其中包括:数据流机器、向量机、VLIW(非常长指令字)机器、SIMD(单指令,多数据)处理器阵列、心动阵列(systolic array)、共享内存的多处理
器、分布式内存的多处理器。每种体系结构概念的发展都伴随着相应编译器技术的研究和发展。
1.5.4 程序翻译
- 二进制翻译
编译器技术可以用于把一个机器的二进制代码翻译成另一个机器的二进制代码,使得可以在一个机器上运行原本为另一个指令集编译的程序。
- 硬件合成
硬件设计也可以使用高级硬件描述语言描述的,这些语言有Verilog和VHDL(Very high-speed integrated circuit Hardware Description Lan-guage,甚高速集成电路硬件描述语言)。
- 数据查询解释器
查询语言,特别是SQL语言(Structured Query Language,结构化查询语言)被用来搜索数据库。数据库查询由包含了关系和布尔运算符的断言组成。它们可以被解释,也可以编译为代码,以便在一个数据库中搜索满足这个断言的记录。
- 编译然后模拟
模拟是在很多科学和工程领域内使用的通用技术。它用来理解一个现象或者验证一个设计,模拟器的输入通常包括设计描述和某次特定模拟运行的具体输入参数。
1.5.5 软件生产率工具
- 类型检查
类型检查是一种有效的,且被充分研究的技术,它可以被用于捕捉程序中的不一致性。它可以用来检测一些错误,比如,运算被作用于错误类型的对象上,或者传递给一个过程的参数和该过程的范型(signature)不匹配。
- 边界检查
包含了自动区间检查的安全的语言编写
- 内存管理工具
自动的内存管理消除了所有的内存管理错误(比如内存泄漏)。这些错误是C或C++程序中问题的主要来源之一。
1.6 程序设计语言基础
1.6.1 静态和动态的区别
静态策略(staticpolicy):编译时刻(compile time)做出决定
动态策略(dynamic policy):运行时刻(run time)做出决定。
1.6.2 环境与状态
- 环境(environment)
是一个从名字到存储位置的映射。因为变量就是指内存位置(即C语言中的术语“左值”),我们还可以换一种方法,把环境定义为从名字到变量的映射。
- 状态(state)
是一个从内存位置到它们的值的映射。以C语言的术语来说,即状态把左值映射为它们的相应右值。环境的改变需要遵守语言的作用域规则
- 是一个从内存位置到它们的值的映射。以C语言的术语来说,即状态把左值映射为它们的相应右值。环境的改变需要遵守语言的作用域规则。
1.6.3 静态作用域和块结构
1.6.4 显式访问控制
1.6.5 动态作用域
1.6.6 参数传递机制
- 值调用
在值调用(call-by-value)中,会对实在参数求值(如果它是表达式)或拷贝(如果它是变量)。这些值被放在属于被调用过程的相应形式参数的内存位置上。值调用的效果是,被调用过程所做的所有有关形式参数的计算都局限于这个过程,相应的实在参数本身不会被改变。
- 引用调用
在引用调用(call-by-reference)中,实在参数的地址作为相应的形式参数的值被传递给被调用者。在被调用者的代码中使用形式参数时,实现方法是沿着这个指针找到调用者指明的内存位置。
- 名调用
第三种机制-名调用-被早期的程序设计语言Algol 60使用。它要求被调用者的运行方式好像是用实在参数以字面方式替换了被调用者的代码中的形式参数一样。
1.6.7 别名
引用调用或者其他类似的方法,比如像Java中那样把对象的引用当作值传递,会引起一个有趣的结果。有可能两个形式参数指向同一个位置,这样的变量称为另一个变量的别名(alias)。结果是,任意两个看起来从两个不同的形式参数中获得值的变量也可能变成对方的别名。
1.6.8 1.6节的练习
- 练习1.6.1
w = 13, x = 11, y = 13, z = 11
- 练习1.6.2
w = 9, x = 7, y = 13, z = 11
- 练习1.6.3
- 练习1.6.4 3 (换行) 2
1.7 第1章总结
- 语言处理器:一个集成的软件开发环境,其中包括很多种类的语言处理器,比如编译器、解释器、汇编器、连接器、加载器、调试器以及程序概要提取工具。
- 编译器的步骤;一个编译器的运作需要一系列的步骤,每个步骤把源程序从一个中间表示转换成为另一个中间表示。
- 机器语言和汇编语言:机器语言是第一代程序设计语言,然后是汇编语言。使用这些语言进行编程既费时,又容易出错。
- 编译器设计中的建模:编译器设计是理论对实践有很大影响的领域之一。已知在编译器设计中有用的模型包括自动机、文法、正则表达式、树型结构和很多其他理论概念。
- 代码优化:虽然代码不能真正达到最优化,但提高代码效率的科学既复杂又非常重要。它是编译技术研究的一个主要部分。
- 高级语言:随着时间的流逝,程序设计语言担负了越来越多的原先由程序员负责的任务,比如内存管理、类型一致性检查或代码的并发执行。
- 编译器和计算机体系结构:编译器技术影响了计算机的体系结构,同时也受到体系结构发展的影响。体系结构中的很多现代创新都依赖于编译器能够从源程序中抽取出有效利用硬件能力的机会。
- 软件生产率和软件安全性:使得编译器能够优化代码的技术同样能够用于多种不同的程序分析任务。这些任务既包括探测常见的程序错误,也包括发现程序可能会受到已被黑客们发现的多种入侵方式之一的伤害。
- 作用域规则:一个x的声明的作用域是一段上下文,在此上下文中对x的使用指向这个声明。如果仅仅通过阅读某个语言的程序就可以确定其作用域,那么这个语言就使用了静态作用域,或者说词法作用域。否则这个语言就使用了动态作用域。
- 环境:名字和内存位置关联,然后再和值相关联。这个情况可以使用环境和状态来描述。其中环境把名字映射成为存储位置,而状态则把位置映射到它的值。
- 块结构:允许语句块相互嵌套的语言称为块结构的语言。假设一个块中有一个x的声明D而嵌套于这个块中的块B中有一个对名字x的使用。如果在这两个块之间没有其他声明了x的块,那么这个x的使用位于D的作用域内。
- 参数传递:参数可以通过值或引用的方式从调用过程传递给被调用过程。当通过值传递方式传递大型对象时,实际被传递的值是指向这些对象本身的引用。这样就变成了一个高效的引用调用。
- 别名:当参数被以引用传递方式(高效地)传递时,两个形式参数可能会指向同一个对象。这会造成一个变量的修改改变了另一个变量的值。