背景
编译原理系列演示代码多以c语言为主、不会c的同学需要学习一下c,不用太深入能懂数据类型、能看懂c的代码就行
一、计算机程序设计语言及编译
计算机的程序设计语言分为三个层次,分别是:
-
机器语言
- 可以直接被计算机理解的语言,计算机只能理解0、1两个数字,因此由机器语言编写的程序就是由0和1构成的序列,比如某机器语言的指令
arduinoC706 0000 0002 //使用的是16进制的形式 /** *C706 操作码,表示移入操作 *0000 操作数,内存地址 *0002 操作数,被操作的数值 */ //该条指令的意思是把 0002存放到0000
- 由上面的这个例子可以看出机器语言和人类语言的表达习惯相差甚远,生活中更多使用的是10进制数,而机器使用的是二进制和16进制数
- 同时程序员还需要记住上面各个数代表的含义、操作等,
- 基于以上使得机器语言处理一个难记忆、难编写、难阅读、与人类表达习惯相去甚远等特点
-
汇编语言
- 汇编语言相比机器语言好好理解一些,汇编语言引入了助记符,例如
arduinoMOV X,2 //MOV 来自英语单词 move ,这就是助记符、表示移入的意思 //X 表示地址 比如 0000 //2 表示被操作的数值 2 //该条指令与上面的机器指令实现的是同一种功能,把2存在到0000,但是引入了助记符号,更加的直观,但是依赖特定的机器,程序员需要熟悉机器的特性。对于非计算机人员难度较高
- 基于上汇编语言存在依赖特定机器、对非计算机人员不友好、编写效率低等特点
-
高级语言
- 基于以上两种语言的痛点,出现了高级语言,高级语言类似自然语言,来编写程序
inix=2
- 上述程序非常简单的便完成了赋值操作,高级语言更加接近人类的表达习惯、不再依赖特定的机器了、编写的效率高
但是高级语言是没办法在计算机上直接执行的,需要翻译
如上图所以这个翻译的过程就称之为编译
什么是编译
将高级语言翻译成汇编语言或者机器语言的过程
学习编译原理就是学习编译器的构成原理的编译技术,核心是编译器如何把高级语言编译成机器语言的
二、编译器在语言处理系统的位置
为了建立可执行的目标程序,除了编译器以外还需要其他程序
-
预处理器(Preprocessor)
- 写过程序的同学都知道,大多数的高级语言都是模块化的语法,程序并不在一个文件中,而预处理器的作用就把存储在不同文件中的源程序(源代码)聚合在一起,把被称为宏的缩写语句转换为原始语句
-
编译器
-
汇编器
- 处理预处理聚合的程序
-
可重定位的机器代码
- 可重定位的意思是,在内存中存放的起始位置L不是固定的,代码中的地址都是相对于这个起始地址的相对地址
- 起始位置+相对地址=绝对地址
- 上面的概念需要理解计算机内存的概念
-
链接器/加载器
- 由于汇编器和编译器生成的程序是可重定位的,因此需要加载器来修改可重定位地址
程序整体编译流程图
大型程序通常会分为多个部分进行编译,因此可重定位的机器代码可能需要和其他可重定位的目标程序以及库文件链接生成可执行代码,这一过程是由链接器完成的,同时链接器还负责解决外部地址问题
外部地址 指一个文件的代码可能会引用另一个文件中数据对象,这个对象就是外部地址
三、编译系统的结构
前面有讲到编译的本质就是翻译的过程
高级语言通过编译器转换汇编语言程序或者机器语言程序,那么机器是如何翻译的呢?
这里可以借鉴自然语言的翻译过程
比如有如下一段英文
In the room, he broke a window with a hammer.
把这段英语翻译为汉语,这里英语就类比高级语言程序,汉语类比汇编程序/机器语言
补充一个概念
源语言:需要翻译高级语言,比如java,javascript等
目标语言:翻译器翻译后的语言,比如汇编语言
先体验英文翻译的过程
- 上述句子输入
- 分析句子核心语法,比如英语的谓语动词 broke
- 的知了打的动作就会分析是谁打的,打的对象是谁,结果如何,这些都可以通过分析broke的上下文获得
- 由于broke是主动语态,所以he就是实事者,window就是受事者
- 反过来如果broke是被动语态,那么 he就是受事者,window就是实事者,当然这个场景下不可能出现
- window with a hammer为补语、In the room为状语
上面这种分析broken就能分析出前后的名词性成分与语义关系
句子语义图
由上可知动作打与人物he、锤子、地点、目标有四条边,这四条边就是与打的关系
he -- 动作的实事者
window窗 -- 动作的受事者
hammer锤子-- 动作采用的工具
room -- 动作发生的地点
根据上面的语义分析就能使用汉语翻译完整的句子了
在房间里,他使用锤子打碎了一扇窗户
以上就完成了翻译的过程了
句子语义图起始可以独立与语言之外,比如汉语翻译英语可以抽象出这张图、德语翻译等等都可以,相当于有了这张抽象图可以随便翻译任何语言
-
编译第一步是语义分析
那么何为语义分析呢?
- 上面英语翻译为汉语的过程中,为了进行语义分析,首先要划分句子成分
又是根据什么来识别句子中的短语呢(语法分析)?
- 可以通过英语语法来划分句子成分,比如状语、主谓宾
又是通过什么来支撑语法划分的呢?
- 还是以上面翻译为例,通过词性可以识别各类短语、比如介词+冠词+名字可以构成状语短语(in the room),又比如冠词+名字 = 宾语 (a window)
综上所述得到了三个重要的概念语义分析 语法分析 词法分析
起始编译器工作就是这三个步骤
编译器结构
其中从词法分析器到中间代码生成器为编译器的分析部分,编译器的前端与源语言相关
目标代码生成器和机器相关代码优化器为综合部分,编译器的后端,与目标语言相关
前端部分通常会一起进行比如在进行语法分析的同时进行语义分析
以上就是编译器的大体结构
三、词法分析(概述)
词法分析是编译的第一个阶段。
补充背景:大家有想过程序被编译器拿的的是核心是什么吗?
其实编译器首先拿到的程序其实就是一个字符串
有了以上背景知识,再看词法分析的主要任务:
从左到右逐行扫描源程序字符,识别出各个单词,确定单词的类型 ,并将识别出的单词转换成统一的机内表示 --**词法单元(token)**形式
什么是机内表示?和词法单元
"机内表示"是计算机科学中的术语,指数据在计算机内存中的存储形式 。在编译原理中,词法分析阶段会将源代码的字符串转换为 结构化的数据对象(Token) ,以便后续阶段(如语法分析)处理。
token是一个二元组它的构成如下,它由种别码和属性值表示,
token:<种别码,属性值>
种别码:类似英语的中词性,比如名词、动词、形容词
属性值:各个词性中具体的词,比如apple属于名词
程序设计中的单词有哪些类型呢?
单词类型 | 种别 | 种别码 |
---|---|---|
关键字 | if、esle、while... |
一词一码,没有属性值 |
标识符 | 变量名、数组... |
多词一码,有属性值(为自己的标识值) |
常量 | 整型、浮点... |
一词一码,没有属性值 |
运算符 | 算术(+、-),赋值(=)... |
一词一码,没有属性值 |
界限符 | {} |
一词一码,没有属性值 |
举个例子
输入以下程序
scss
while(value!=100){num++}
输出token
vbnet
while <WHILE, - >
( <SLP, - >
value <IDN, value >
!= <NE, - >
100 <CONST, 100 >
) <SRP, - >
{ <LP, - >
num <IDN, num >
++ <INC, - >
; <SEMI, - >
} <RP, - >
上面输出的token中num和value都是标识符、属性值是本身
四、语法分析(概述)
语法分析器(parser)从词法分析器输出的token序列中标识出各类短语,并构造词法分析树(parse tree),语法分析树描述句子的语法结构
举个例子
- 赋值语句分析树
ini
position = inital + rate * 60
分析树
表达式和表达式可以构成一个更大的表达式
这里需要一些数据结构的知识,上面的这颗树可以使用先根遍历的形式构建出上面的赋值语句树
- 变量声明语句分析树
文法
xml
<D> -> <T> <IDS>
<T> -> int | real| char| bool
<IDS> -> id|<IDS>,id
<id> --> a,b,c(单个变量名)...
节点 | 对应文法规则 | 说明 |
---|---|---|
D | <D> -> <T> <IDS> ; |
整个变量声明语句。 |
T | <T> -> int |
类型关键字 int 。 |
IDS | <IDS> -> <IDS>, id |
递归处理逗号分隔的变量名。 |
id(a/b/c) | <IDS> -> id |
单个变量名(如 a )。 |
输入
int a,b,c;
分析树
五、语义分析(概述)
高级语言的语句分为两类
- 声明语句;声明一些数据对象并且为它们取名为标识符
- 可执行语句
语义分析的主要任务
-
收集标识符的属性信息
收集的信息有哪些
-
种属(kind)
简单变量、复合变量(数组、对象...)、过程、...
-
类型(type)
整型、实型、字符型、布尔型、指针型...
-
存储位置、长度
-
值
-
作用域
-
参数和返回信息 语义分析收集到的这些信息都会存放到 符号表(Symbol Table) 的数据结构中,符号表后续写一篇文章专门讲解符号表
-
-
语义检查
- 变量或者过程未经声明就使用
- 变量或者过程名重复声明
- 运算分量类型不匹配、比如将一个过程和数组相加
- 操作符与操作数类型不匹配
补充什么是"过程"?
在编程语言中, "过程" (Procedure)是一个通用术语,通常指 一组执行特定任务的代码块,可以被调用。不同语言中可能有不同的名称:
- 函数(Function):如 C、JavaScript。
- 方法(Method):如 Java、Python。
- 子程序(Subroutine):如 Fortran
六、中间代码生成和编译器后端(概述)
常见的中间表示形式
-
三地址码
三地址码由类似汇编语言的指令序列组成,每个指令最多有三个操作数
指令类型 | 三地址码(TAC) | 四元组表示 | 示例 |
---|---|---|---|
赋值指令 | x = y op z |
(op, y, z, x) |
a = b + c → (+, b, c, a) |
一元运算 | x = op y |
(op, y, _, x) |
neg = -a → (-, a, _, neg) |
条件跳转 | if x relop y goto L |
(relop, x, y, L) |
if a < b goto L2 → (<, a, b, L2) |
无条件跳转 | goto L |
(goto, _, _, L) |
goto L3 → (goto, _, _, L3) |
函数调用 | x = call f, n |
(call, f, n, x) |
res = call func, 2 → (call, func, 2, res) |
数组赋值 | x = y[i] |
(array, y, i, x) |
a = b[3] → (array, b, 3, a) |
数组存储 | x[i] = y |
(store, x, i, y) |
c[5] = d → (store, c, 5, d) |
地址操作 | x = &y |
(&, y, _, x) |
ptr = &a → (&, a, _, ptr) |
间接寻址 | x = *y |
(*, y, _, x) |
val = *ptr → (*, ptr, _, val) |
关系运算 | x = y relop z |
(relop, y, z, x) |
flag = a != b → (!=, a, b, flag) |
逻辑运算 | `x = y | z` |
代码举例
ini
int a = 5;
int b = 10;
if (a < b) {
b = 0;
}
三地址码
ini
t1 = 5
a = t1
t2 = 10
b = t2
if a < b goto L1
goto L2
L1:
t3 = 0
b = t3
L2:
- 语法结构树/语法树(与前面的语法结构树不是一个东西)
编译器的结构
将目标代码生成以源程序的中间表示形式作为输入,并把它映射到目标代码
中间代码生成和编译器后端和概念都比较抽象,它们的代码很像汇编程序,将在后续的章节展开讲解。