编译原理 引论

以下内容来自西安交通大学吴晓军老师的ppt,仅供学习使用,请勿转载。

引论

一、什么叫编译程序

1. 编译程序历史

编译程序是系统软件资格最老的成员之一

三大系统软件:数据库、操作系统、编译器

编译理论和技术发展迅速,已经比较成熟,已形成了一套较为系统化的编译理论与方法。

2. 编译理论与其他课程关系

image-20220117081525956

3. 编译理论的应用

编译理论的很多想法和技术可以用于一般软件的设计:

  • 有穷状态技术:文本编辑程序(比如word,需要拼写检查、字数统计)、情报检索、模式识别(用计算的方法根据样本的特征将样本划分到一定的类别中去,比如人脸识别、语音识别、文字识别)

  • 上下文无关文法和语法制导翻译:建立多种文本处理程序

  • 代码优化技术:程序校验、由非结构化到结构化的程序转换

4. 翻译程序

翻译程序是一种程序,输入是某种语言的一系列语句(源语言程序),而其输出是另一种语言(目标语言程序)的一系列语句。

翻译程序没有说语言有高低,只是强调它们不一样。

5. 编译程序

编译程序把用高级语言写的源程序作为数据接收,经过翻译转换,产生面向机器的代码作为输出。

不一定会一步转化到位,可能还需要汇编程序装配程序进一步加工,得到目标程序。

高级、低级是对于语言距离底层机器硬件的距离而言的。

6. 翻译和编译

翻译就只是一种语言到另一种语言的转换,编译是针对输入输出有一些限制(必须是从高级源程序低级目标程序)。

二、编译过程概述

1. 编译过程的组成

源程序 -> 词法分析 ->单词符号

单词符号 -> 语法分析 -> 语法单位

语法单位 -> (语义分析和)中间代码生成 -> 中间代码

中间代码 -> 代码优化 -> 优化后的中间代码

优化后的中间代码 -> 目标代码生成 -> 目标代码

2. 词法分析

任务: 输入源程序,扫描、分解字符串,识别出一个个单词,不仅是识别出,还要进行分类(基本字、标识符、算符、界符、常数)。

该过程也就是将源程序字符串转化为单词符号

例:

image-20220117085041339

基本字就是高级语言中的一些保留字和关键字
这些自己定义的变量名被称为标识符
界符也就是能够把标识符分开的符号(运算符也可以视为特殊的界符,但是这里称之为算符
程序里面直接写出来的数字被称为常数

3. 语法分析

任务:在词法分析的基础上,将单词符号串转化为语法单位(如短语、子句、句子、程序段、程序),并确定整个输入串是否构成语法上正确的程序。

也就是将单词符号转化为语法单位。

主要理论依据是上下文无关文法

例子:

image-20220117134203600

  1. 首先,变量、常数及其运算结果均是表达式,如下图所示。(比如1、100、10*k然后+I等等)

image-20220117134356430

  1. 然后赋值语句的形式变为变量:=表达式

image-20220117134457512

  1. 然后多个赋值句构成语句块:

image-20220117134533060

  1. 表达式可以作为循环的初值和终值

image-20220117134616385

  1. 简单数值变量可以作为循环的控制变量

image-20220117134656292

  1. 然后根据FOR循环语句的语法规则,就可以知道这是一个正确的循环语句,语法分析就完成了。

4. 中间代码生成(语义分析)

任务:对语法分析所识别出的各类语法范畴,分析其含义,并进行初步翻译(产生中间代码)。

也就是语法范畴到中间代码,但是并非所有语法范畴都会对应到中间代码。这一阶段的依据是语义规则,理论基础是属性文法

例子:

  1. 对于上面的循环语句,首先要找到循环语句和出口语句,彼此相连地被定义(开头和结尾):

image-20220117135251180

  1. 包括循环语句的开始到同一控制变量的第一个出口语句的那些语句的自然序列称为一个循环块。

image-20220117135357732

块嵌套不可交叉,控制变量也不可以同名。也就是说只能一个循环块的所有内容都包在另一个循环块中,不能只有一部分(如下图,左边不正确,右边正确)。只有语义是正确的,才会去进行中间代码生成。

image-20220117135550229

  1. 缺省了控制变量的步长,会根据语义规则补全,默认为1。

image-20220117140019135

  1. 然后我们开始中间代码生成,这里采用了四元式,四元式是中间代码的一种形式,格式如下:
算符左操作数右操作数结果

比如:

  1. 上面的K:=1可以写成(:=,1, ,K)

  2. To 100(也就是100<k的话则跳转)可以写成(j<,100,K, ),最后结果为空是因为转移目标还不知道。

  3. 然后是10*K,可以写成(*,10,K,T1),其中T1是一个临时变量,四元式使用临时变量是十分阔绰的,只要认为需要使用临时变量,则用临时变量。

  4. 后面是M:=I+T1,这里是直接用上面的临时变量替换了写的,也是一个赋值语句,也就是(+,I,T1,M)。实际上,这里应该是2句,先使用临时变量T保存结果(+,I,T1,T),然后再赋值(:=, T, ,M),可以理解为优化了一下。

  5. 之后的结果类似。

  6. 到最后NEXT K也就是(+,K,1,K)

  7. 然后跳回到上面的2,也就是(j, , ,(2)),继续进行比较。

  8. 那么当语句循环结束之后,就可以知道循环之外的第一条指令(9)。于是可以填入上面(2)中(j<,100,K,(9))

总的过程也就是:

(:=,1, ,K)
(j<,100,K,(9))
(*,10,K,T1)
(+,I,T1,M)
(*,10,K,T2)
(+,J,T2,N)
(+,K,1,K)
(j, , ,(2))
(9)

然后采用一些助记符,变成如下:

K := 1
if 100<K goto (9)
T1 := 10 * K
M := I + T1
T2 := 10 * K
N := J + T2
K := K + 1
goto(2)
(9)

5. 代码优化

任务:对于代码(主要是中间代码)进行加工变换,以期能够产生更为高效的目标代码。

依据是程序等价变换规则,主要理论基础是数据流方程。

我们在上面得到的中间代码进行优化,主要是将里面的乘法改成加法,因为乘法比加法用的时间长很多。先给M和N赋初值,然后再每次+10,优化后的中间代码如下:

M := I
N := J
K := 1
if 100<K goto (9)
M := M + 10
N := N + 10
K := K + 1
goto(2)
(9)

6. 目标代码生成

任务:将已经优化过的中间代码变换成特定机器上的低级语言代码。

目标代码形式可以是:绝对指令、可重定位指令、汇编指令

依据是硬件体系结构和指令系统。

下面这段示例只是帮助大家理解,并不是真实的汇编指令,其实和ARM或者x86的汇编也十分类似,如果学过ARM汇编的话很好理解。

	LD R1,I
	LD R2,J
	LD R0,1
L1:	CMP 100,R0
	J< L2
	ADD R1,10
	ADD R2,10
	ADD R0,1
	J L1
L2:	ST R1,M
	ST R2,N

三、编译程序的结构

1. 编译程序总框

image-20220117094507660

上面每一个过程都包含了一个工具,如词法分析对应了词法分析器,语法分析对应语法分析器,以此类推。

  • 词法分析器:又称扫描器,输入源程序,进行词法分析。

  • 语法分析器:简称分析器,对单词符号串进行语法分析(根据语法规则推导或规约),判断输入串是否语法正确,

  • 语义分析与中间代码产生器:根据语义规则对语法分析器规约出的语法单位进行语义分析,并把他们翻译成一定形式的中间代码。

    有些编译程序识别出各类语法单位后,构造并输出一棵表示语法结构的语法树,然后根据语法树进行语义分析和中间代码产生。

  • 优化器:对中间代码进行优化处理

  • 目标代码生成器:把中间代码翻译成目标程序。

除了上面的5个功能模块,一个完整的编译程序还应该包括表格管理出错处理

2. 表格与表格管理

编译程序在工作过程中需要一系列表格来登记源程序的各类信息和编译各阶段的进展状况。

编译各阶段都需要维持表格和进行表格管理。

建表的技术支持是数据结构。

表格的分类、结构、处理方法决定于语言和机器,还有优化措施。

表格有:符号名表、常数表、标号表、入口名表、过程引用表、循环表、中间代码表等等。

其中最重要的是符号表,用来登记源程序中出现的每个名字和名字的各个属性。名字可以是比常量名、变量名、过程名等,变量名的属性有类型、内存大小、地址等等。

编译程序在处理到名字的定义出现时,会把名字的各种属性填入符号表;在处理到使用时,对名字的属性进行查证。当扫描器扫描到一个名字的时候,并不能完全确定它的属性,比如它的类型要等到语义分析时确定,而地址要等到生成目标代码才能确定。

3. 出错处理

一个好的编译程序应该:

  • 全:最大限度发现错误
  • 准:准确指出错误的性质和发生地点
  • 局部化:将错误的影响限制在尽可能小的范围内

自动矫正固然好,但是代价高,还是让程序员自己改错。

错误类型:

  • 语法错误:不符合语法(或词法)规则的错误,如拼写错误、括号不匹配。

  • 语义错误:不符合语义规则的错误,如说明错误、类型不一致。

4. 遍(pass)

遍是对源程序或源程序的中间结果从头到尾扫描一遍,并作有关的加工处理,生成新的中间结果或目标程序。

在转化为中间结果之后,比如进行完了语法分析生成了中间代码,那么这时候就不是针对源程序进行扫描了,而是针对中间代码。最后一遍的输出是目标程序。

那么是不是对于编译过程的5个阶段就对应了5遍呢,这个是不一定的,早期可能由于硬件的限制是这么做的,但是后来进行了一些优化。最经典的做法是

  • 词法分析、语法分析和中间代码生成是一遍,其中语法分析器处于核心地位
  • 然后中间代码生成到代码优化扫描一遍,进行局部优化。
  • 代码优化到目标代码生成是一遍全局优化。
  • 最后目标代码生成是一遍。
image-20220122172408440

5. 编译前端和后端

编译的前端和后端是根据与源语言和目标机器有关还是无关来区分的。

编译前端主要由与源语言有关但与目标机无关的那些部分组成。

编译后端包括编译程序中与目标机有关但是和源语言无关的那些部分。

image-20220122173751518
图 橙色框出来的属于编译前端,紫色属于编译后端

四、编译程序生成

1. 编译程序的构造工具

以往编译程序依靠机器语言或者汇编语言构造

现在更多的是采用高级语言构造编译程序,当然,有时为了提高编译的效率,将一部分核心使用机器语言或者汇编语言构造。

2. T型图

image-20220122175400688

3. 用高级语言L1构造编译程序

假设我们已经有用A机器代码实现的高级语言L1的编译程序,比如在x86上的C语言编译程序。

image-20220122175718239

用高级语言L1编写另一个高级语言L2的编译程序,比如用C语言编写go语言的编译程序(如下图中蓝色部分)。

image-20220122180149655

蓝色这部分T型图是用C语言写的go语言的编译程序,该程序是不可以执行的(需要编译)。

对这个用L1语言写的L2语言的编译器,经过L1语言的编译器,那么我们就可以得到L2语言到A的编译程序(下图中绿色部分)。

image-20220122180444172

4. 编译程序的移植

思考这样一个问题,我们现在有一个在x86机器上的C语言编译程序,那么我们怎么得到一个在Arm上的C语言编译程序,这个就称之为编译程序的移植。

更普遍地说,就是我们有一个在A机器上可以运行的L语言的编译程序,那么我们如何得到一个在B机器上可以运行的L语言的编译程序。

首先,我们用L语言写一个将L语言编译为B机器语言的编译程序(2),然后通过A机器上的L语言的编译程序(1),就可以得到一个能在A机器上运行的能够将L语言编译为B机器语言的编译程序(3)。但是这个程序不能在B机器上运行,那么我们再将L语言写的将L语言编译为B机器语言的编译程序(2)作为输入给(3)这个程序,于是就可以得到一个能够在B机器上运行的将L编译成B机器语言的编译程序(4)。

image-20220122183508465

最后得到的(4)这个编译程序就可以在B机器上运行,将L语言编译成可执行的B代码。

5. 自编译方式

所谓自编译也就是先对语言的核心功能构造一个小的编译程序,由于比较小,所以可以使用机器语言实现,比如这个编译程序就可以编译C语言的变量定义、赋值语句,或者if、for这些简单的分支控制、循环语句等。那么我们就可以使用这些工具,再构造能够编译更多语言成分的较大的编译程序,最后得到整个编译程序。

6. 构造工具

构造编译程序的工具称为编译程序-编译程序编译程序产生器或者翻译程序书写系统

自动产生扫描器(词法分析器):LEX、FLEX

自动产生语法分析器:YACC BISON

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值