经过编译所得的目标程序是_编译原理(龙书)笔记-第一章(1.1-1.3节)

74b5a9f802ecb207a71ce6bdf54ae6e5.png

第一章 引论

关于本书

介绍设计和实现编译器的方法,用于构建面向多种语言和机器的翻译器的一些基本思想。目的是教授编译器设计中使用的根本思想和方法论。本书并不是以读者学习建立一个最新的语言处理系统时可能用到的所有算法和技术为导向,而是希望读者将获得必要的基础知识和理解,学会建立一个相对简单的编译器。

1.1 语言处理器

导读:1.1讲解编译器和解释器的定义、作用以及各优点。介绍语言处理系统。

编译器是一种程序,可以阅读源程序(某一种语言编写)并将其翻译成目标程序(目标语言编写)。编译器的重要任务之一是报告它在翻译过程中发现的源程序中的错误。

a5709043ee8b8b6b3d8db42e420d8874.png

如果目标程序是一个可执行机器语言程序。那么它就可以被用户调用,处理输入并产生输出。

ae5a4c316dc19cbbc27534dd48dda2b0.png

解释器是另一种常见的语言处理器。它并不通过翻译的方式生成目标程序。从用户的角度来看,解释器直接利用用户提供的输入执行源程序中指定的操作。

1ed11788447384c609e65e6ca22bdfff.png

例1.1 Java的语言处理器

Java的语言处理器结合了编译和解释过程。如图1.4所示。

3cf8924d3f7fa683539755104b28eb04.png

一个Java源程序首先会被编译成一个被称为字节码(bytecode)的中间表达式。然后有一个虚拟机对得到的字节码加以解释进行。这样的安排好处之一是在一台机器编译得到的字节码可以在另一台机器上解释执行。通过网络就可以完成机器之间的迁移。为了更快地完成输入到输出的处理,有些被称为即时(just in time)编译器的Java编译器在运行中间程序处理输入的前一刻首先把字节码翻译成为机器语言,然后再执行程序。

一个语言处理系统(如图1-5所示)

7058e8307a840d238d6c44813b250953.png

除了编译器之外,创建一个可执行的目标程序还需要一些其他程序。一个源程序可能被分割成为多个模块,并存放于独立的文件中。把源程序聚合在一起的任务有时会由一个被称为预处理器(preprocessor)的程序独立完成。预处理器还负责把那些称为宏的缩写形式转换为源语言的语句(总结:预处理器负责把源程序聚合在一起,并把宏转换为源语言的语句)

然后,将经过预处理的源程序作为输入传递给一个编译器。编译器可能产生一个汇编语言程序作为其输出,因为汇编语言比较容易输出和调试。接着,这个汇编语言程序由称为汇编器(assembler)的程序进行处理,并产生可重定位的机器代码(总结:汇编器负责把汇编语言进行处理,并产生可重定位的机器代码)

大型程序经常被分成多个部分进行编译,因此,可重定位的机器代码有必要和其他可重定位的目标文件以及库文件连接到一起,形成真正在机器上运行的代码。一个文件中的代码可能指向另一个文件中的位置,而链接器(linker)能够解决外部内存地址的问题。最后,加载器(loader)把所有的可执行目标文件放到内存中执行

1.2 一个编译器的结构

导读:1.2先介绍编译器的结构,然后按照编译器的结构顺序逐个介绍每个步骤。

编译器 = 分析部分(作为编译器的前端) + 综合部分(作为编译器的后端)

分析部分:1、将源程序分解为多个组成要素。2、将组成要素通过语法结构创建该源程序的一个中间表示。3、检查源程序有没有按照正确的语法结构和语义,如果没有则必须提供有用的信息供用户改正。4、收集有关源程序的信息存放在一个称为符号表的数据结构。5、符号表和中间表示形式一起传送给综合部分。

综合部分:根据中间表示和符号表中的信息来构造用户期待的目标程序。

编译器的步骤(如图1-6所示)

ba6f4ea307c33973811dc247d0b643db.png

有些编译器在前端和后端之间有一个与机器无关的优化步骤。这个优化步骤的目的是在中间表示之上进行转换,以便后端程序能够生成更好的目标程序。如果基于未经过次优化步骤的中间表示来生成代码,则代码的质量会受到影响。因为优化是可选的,所以图1-6中所示的两个优化步骤之一可以被忽略。

1.2.1 词法分析

词法分析器:读入组成源程序的字符流,并且将它们组织成为有意义的词素(lexeme)的序列。词法分析器会将每个词素生成如下形式的词法单元(token)作为输出:

<token-name, attribute-value>

词法单元生成后会被传送到下一个步骤,即语法分析。

词法单元 <token-name, attribute-value>:第一个分量token-name是一个由语法分析步骤使用的抽象符号,而第二个分量attribute-value指向符号表中关于这个词法单元的条目。符号表条目的信息会被语义分析代码生成步骤使用。

下面用例子说明词法分析器如何将源程序的字符流生成词素,并映射成词法单元。

例子:源程序中的某个赋值语句 position = initial + rate * 60 (1. 1)

这个赋值语句中的字符可以组成词素,并映射成为词法单元。

1)position是一个词素,被映射成词法单元<id, 1>,其中id是表示标识符(identifier)的抽象符号,而 1 只想符号表中position对应的条目。一个标识符对应的符号表条目存放该标识符有关的信息,比如它的名字和类型。

2)赋值符号 = 是一个词素,被映射成词法单元<=>。因为这个词法单元不需要属性值,所以我们省略了第二分量。也可以使用assign这样的丑行符号作为词法单元的名字,但是为了标记上的方便,我们选择了使用词素本身作为抽象符号的名字。

3)initial是一个词素,被映射成词法单元<id, 2>,其中2是指initial对应的符号表条目。

4)+是一个词素,被映射成词法单元<+>。

5)rate是一个词素,被映射成词法单元<id, 3>,其中3是指rate对应的符号表条目。

6)*是一个词素,被映射成词法单元<*>。

7)60是一个词素,被映射成词法单元<60>。(注:其实应该是<number, 4>,但在后续的第2章才涉及到数字的词法单元,故先用<60>)

分隔词素的空格会被词法分析器忽略掉。

下面图1-7(这张图很重要,将贯穿整个1.2节)给出经过词法分析后,赋值语句1.1被表示成如下的词法单元序列:

<id, 1> <=> <id, 2> <+> <id, 3> <*> <60> (1. 2)

在这个表中,词法单元名=、+和*分别表示赋值、加法运算发、乘法运算符的抽线符号。

50efaa997f91f10789d5bc3d20a2f45b.png

1.2.2 语法分析

编译器的第2个步骤:语法分析(syntax analysis)或解析(parsing)。语法分析器使用由词法分析器生成的各个词法单元的第一个分量来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元流语法结构。一个常用的表示方法是语法树(syntax tree),树中的每个内部结点表示一个运算,而该结点的子结点表示该运算的分量。在图1-7中,词法单元流(1. 2)对应的语法树被显示为语法分析器的输出。

小结:1)语法分析器的作用:根据语法结构,将各个词法单元的第一个分量创建成树形的中间表示,并且将中间表示输出。2)中间表示=词法单元流(各个词法单元里的第一个分量)+语法结构。3)中间表示一般常用的是语法树。4)语法树中每个内部结点表示运算,而该结点的子结点表示该运算的分量。

761fbf314842fb5da0fb605332dc4834.png
语法分析器生成的语法树(syntax tree)

这棵树显示了赋值语句 position = initial + rate * 60 中各个运算的执行顺序。这棵树有一个符号为 * 的内部结点,<id, 3>是它的左子结点,整数60是它的右子结点。结点<id, 3>表示标识符rate。标号为*的结点指明了我们必须首先把rate的值与60相乘。标号为+的结点表明我们必须把相乘的结果和initial的值相加。这棵树的根节点的标号为=,它表明我们必须把相加的结果存储到标识符position对应的位置上去。这个运算顺序和通常的算术规则相同,即乘法的优先级高于加法,因此乘法应该在加法之前计算。

1.2.3 语义分析

语义分析器(semantic analyzer)使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。它同时也收集类型信息,并把这些信息存放在语法书或符号表中,以便在随后的中间代码生成过程中使用。

语义分析的一个重要部分是类型检查(type checking)。编译器检查每个运算符是否具有匹配的运算分量。比如,很多程序设计语言的定义中要求一个数组的下表必须是整数。如果用一个浮点数作为数组下标,编译器就必须报告错误。

程序设计语言可能允许某些类型转换,这被称为自动类型转换(coercion)。比如,一个二元算术运算符可以应用一对整数或者一对浮点数。如果这个运算符应用于一个浮点数和一个整数,那么编译器可以把该整数转换(或者说自动类型转换)成为一个浮点数。

d28a4b403d9e839c54a8ff86c0f1e1f9.png
自动类型转换

上图中显示了一个这样的自动类型转换。假设position、initial和rate已被声明为浮点数类型,而词素60本身形成一个整数。图1-7中的语义分析器的类型检查程序发现运算符*被用于一个浮点数rate和一个整数60。在这种情况下,这个整数可以被转换成为一个浮点数。请注意,在图1-7中,语义分析器输出中有一个关于运算符inttofloat的额外结点。inttofloat明确地把它的整数参数转换为一个浮点数。

1.2.4 中间代码生成

在把一个源程序翻译成目标代码的过程中,一个编译器可能构造出一个或多个中间表示。这些中间表示可以有多种形式。语法树是一种中间表示形式,它们通常在语法分析和语义分析中使用。

在源程序的语法分析和语义分析完成之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。我们可以把这个表示看作是某个抽象机器的程序。该中间表示应该具有两个重要的性质:它应该易于生成,且能够被轻松地翻译为目标机器上的语言。

在第6章,我们将考虑一种称为三地址代码(three-address code)的中间表示形式。这种中间表示由一组类似于汇编语言的指令组成,每个指令具有三个运算分量。每个运算分量都像一个寄存器。图1-7中的中间代码生成器的输出是如下的三地址代码序列:

a3eb675440e34db49b7448b307333611.png

关于三地址指令,有几点是值得专门指出的。首先,每个三地址赋值指令的右部最多只有一个运算符。因此这些指令确定了运算完成的顺序。在源程序1.1中,乘法应该在加法之前完成。第二,编译器应该生成一个临时名字以存放一个三地址指令计算得到的值。第三,有些三地址指令的运算分量的少于三个(比如上面的序列1.3中的第一个和最后一个指令)。

1.2.5 代码优化

机器无关的代码优化步骤试图改进中间代码,以便生成更好的目标代码。“更好”通常意味着更快,但是也可能会有其他目标,如更短的或能耗更低的目标代码。比如,一个简单直接的算法会生成中间代码(1.3)。它为由语义分析器得到的树形中间表示中的每个运算符都使用一个指令。

使用一个简单的中间代码生成算法,然后再进行代码优化步骤是生成优质目标代码的一个合理方法。优化器可以得出结论:把60从整数转换为浮点数的运算可以在编译时刻一劳永逸地完成。因此,用浮点数60.0来代替整数60就可以消除相应的inttofloat运算。而且,t3仅被使用一次,用来把它的值传递给id1。因此,优化器可以把序列(1.3)转换为更短的指令序列。

08e0c8fc0b36082390232719a7edb8ca.png

不同的编译器所做的代码优化工作量相差很大。那些优化工作做得最多的编译器,即所谓的“优化编译器”,会在优化阶段花相当多的时间。有些简单的优化方法可以极大地提高目标程序的运行效率而不会过多降低编译的速度。

1.2.6 代码生成

代码生成器以源程序的中间表示形式作为输入,并把它映射到目标语言。如果目标语言是机器代码,那么就必须为程序使用的每个变量选择寄存器或内存位置。然后,中间指令被翻译成为能够完成相同任务的机器指令序列。代码生成的一个至关重要的方面是合理分配寄存器以存放变量的值

比如,使用寄存器R1和R2,(1.4)中的中间代码可以被翻译成为如下的机器代码:

8ceca229a9f10a508e4dfd2dd37ec5d2.png

每个指令的第一个运算分量指定了一个目标地址。各个指令中的F告诉我们它处理的是浮点数。代码(1.5)把地址id3中的内容加载到寄存器R2中,然后将其与浮点数60.0相乘。井号“#”表示60.0应该作为一个立即数处理。第三个指令把id2移动到寄存器R1中,而第四个指令把前面计算得到并存放在R2中的值加到R1上。最后,在寄存器R1中的值被存放到id1的地址中去。这样,这些代码正确地实现了赋值语句(1.1)。这里对代码生成的讨论暂时忽略了对源程序中的标识进行存储分配的重要问题,将在后面的章节提到。

1.2.7 符号表管理

编译器的重要功能之一是记录源程序中使用的变量的名字,并收集和每个名字的各种属性有关的信息。这些属性可以提供一个名字的存储分配、它的类型、作用域(即在程序的哪些地方可以使用这个名字的值)等信息。对于过程名字,这些信息还包括:它的参数数量和类型、每个参数的传递方法(比如传值或传引用)以及返回类型。

符号表数据结构为每个变量名字创建了一个记录条目。记录的字段就是名字的各个属性。这个数据结构应该允许编译器迅速查找到每个名字的记录,并向记录中快速存放和获取记录中的数据。

1.2.8 将多个步骤组合成躺

前面关于步骤的讨论讲的是一个编译器的逻辑组织形式。在一个特定的实现中,多个步骤的活动可以被组合成一趟(pass)。每趟读入一个输入文件并产生一个输出文件。比如,前端步骤中的词法分析、语法分析、语义分析,以及中间代码生成可以被组合在一起成为一趟。代码优化可以作为一个可选的趟。然后可以有一个为特定目标机生成代码的后端趟。

有些编译器集合是围绕一组精心设计的中间表示形式而创建的,这些中间表示形式使得我们可以把特定语言的前端和特定目标机的后端相结合。使用这些集合,我们可以把不同的前端和某个目标机的后端结合起来,为不同的源语言建立该目标机上的编译器。类似地,我们可以把一个前端和不同的目标机后端结合,建立针对不同目标机的编译器。

1.2.9 编译器构造工具

这里就直接贴书上的文字。

e5b541adf566c9918d2b12f6dddf69a3.png

1.3 程序设计语言的发展历程

1.3.1 走向高级程序设计语言

这里只说明一些术语

第一代语言是机器语言。

第二代语言是汇编语言。

第三代语言是高级程序设计语言。

第四代语言是为特定应用设计的语言。

第五代语言是基于逻辑和约束的语言。

强制式语言:把程序中指明如何完成一个计算任务的语言的称为强制式(imperative)语言。所有强制式语言中都有用于表示程序状态和语句的表示方法,这些语句可以改变程序状态。

声明式语言:把程序中指明要进行哪些计算的语言称为声明式(declarative)语言。一般函数式语言和带有约束逻辑语言通常被认为是声明式语言。

·诺依曼语言(von Neumann language):指以冯·诺依曼计算机体系结构为计算模型的程序设计语言。

面向对象语言(object-oriented language):指的是支持面向对象编程的语言,面向对象编程是指用一组相互作用的对象组成程序的编程风格。

脚本语言(scripting language):具有高层次运算符的解释型语言,它通常被用于把多个计算过程“粘合”在一起。这些计算过程被称为脚本。

1.3.2 对编译器的影响

这里也是直接贴书上的文本。

027bdec0f0101f946a17a8fa97ac1e47.png

如有错误请在留言指出,不喜勿喷,谢谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值