目录结构
注:提前言明 本文借鉴了以下博主、书籍或网站的内容,其列表如下:
1、参考书籍:《编译原理》第三版 清华大学出版社
1、本文内容全部来源于开源社区 GitHub和以上博主的贡献,本文也免费开源(可能会存在问题,评论区等待大佬们的指正)
2、本文目的:开源共享 抛砖引玉 一起学习
3、本文不提供任何资源 不存在任何交易 与任何组织和机构无关
4、大家可以根据需要自行 复制粘贴以及作为其他个人用途,但是不允许转载 不允许商用 (写作不易,还请见谅 💖)

文章快速说明索引
学习目标:
编译原理作为计算机专业的一门核心课程,一来 其知识体系贯穿 程序设计语言、系统环境 以及 体系结构,可以以相对独立的视角来体现 从软件到硬件 以及 软硬件协同 的整机概念。此外 其理论基础又涉及 形式语言与自动机、数据结构与算法等计算机领域的许多重要范畴,是为联系 计算机科学理论 和 计算机系统的典范课程。 最后,上述的知识体系所涉及的原理和技术 并不仅仅适用于编写编译程序,对许多软件的设计也大有所用。
而编译程序(或编译器 编译系统)在整个计算机科学与技术的发展中也发挥了巨大的作用,是计算机系统的核心支撑软件。
在日常的PostgreSQL数据库内核开发中,深感编译原理的实际重要用途。虽未伤及日常功能开发,但为以后长久来看 思虑再三,最终决定抽上一段时间安排学习编译原理,以补当年之憾 以解当下之困!
学习内容:(详见目录)
我们这里主要学习程序设计语言编译程序构造的一般原理、基本设计方法和主要实现技术:
- 文法、自动机和语言的基础知识
- 词法分析
- 语法分析
- 语法制导的语义计算
- 语义分析
- 中间代码生成
- 运行时存储组织
- 代码优化
- 目标代码生成
学习时间:
2021年07月02日 01:02:46
学习产出:
1、编译原理 学习和总结
2、CSDN 技术博客 1篇
这里需要提前说明的是,我们此次的学习开发环境是:
[mydb@local64 ~]$ cat /etc/redhat-release
redhat-7
[mydb@local64 ~]$ uname -a
Linux local64 3.10.0-693.el7.x86_64 #1 SMP Tue Aug 22 21:09:27 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
[mydb@local64 ~]$
[mydb@local64 ~]$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/local/libexec/gcc/x86_64-pc-linux-gnu/7.1.0/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: ../configure --enable-checking=release --enable-languages=c,c++ --disable-multilib
Thread model: posix
gcc version 7.1.0 (GCC)
[mydb@local64 ~]$
[mydb@local64 ~]$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/local/libexec/gcc/x86_64-pc-linux-gnu/7.1.0/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: ../configure --enable-checking=release --enable-languages=c,c++ --disable-multilib
Thread model: posix
gcc version 7.1.0 (GCC)
[mydb@local64 ~]$
学习开发语言为:C/C++
什么是编译程序
编译程序是现代计算机系统的基本组成部分之一,而且多数计算机系统都含有不止一个高级语言的编译程序。对有些高级语言甚至配置了几个不同性能的编译程序。
语言和翻译:语言是人类交流思想和信息的工具。如自然语言,世界上存在着许多种语言,各国之间要交流信息,就要有各种语言之间的翻译。而计算机语言同样也是丰富多彩的。
从功能上来看,编译程序就是语言翻译程序,而语言翻译程序是指:
把一种语言(源语言)书写的程序 翻译成 另一种语言(目标语言)的 等价程序
在计算机众多程序设计语言的世界里是有很多这样的翻译程序,例如:
- 汇编程序:把汇编语言程序 翻译成 机器语言程序
而诸如源语言是 C/C++ Pascal等高级语言,其翻译对应的目标语言是像 汇编语言 或者 机器语言这样的低级语言,那么这种翻译程序被称之为编译程序。
编译程序作为一个语言翻译程序,也要在翻译过程中检查源程序的语法和语义,报告一些出错和警告信息,进而帮助程序员更正源程序。下面是有关编译程序的专业术语:
- 编译程序的源语言(源程序)
- 编译程序的目标语言(目标程序)
- 编译程序的实现语言
这些术语的英文如下:
- 编译程序 — compiler
- 源语言 — source language
- 源程序 — source program
- 目标语言 — target or object language
- 目标程序 — target or object program
- 实现语言 — implementation language
如上图所示:一个编译程序(可以看成为:黑盒子)的重要性体现在它使得多数计算机用户不必考虑与机器有关的繁琐细节,使程序员和程序设计专家独立于机器,这对于当今机器的数量和种类持续不断地增长的年代尤为重要。上面是我们上帝视角中的编译程序定义,那么如果从计算机系统的角度看,什么是编译程序呢?编译程序是一种软件,是系统软件。通常认为系统软件是居于计算机系统中最靠近硬件的一层,其他软件一般都通过系统软件来发挥作用。系统软件和具体的应用领域无关,如编译系统和操作系统等。编译程序也是一种语言处理系统,即把软件语言书写的各种程序处理成可在计算机上执行的程序。
然而要把软件语言书写的各种程序处理成可在计算机上执行的目标程序,除了编译程序外,还需要一些其它的程序。下面我们来看一下一个高级程序设计语言程序的典型的处理过程,可以从中进一步了解编译程序的作用:
下面来解释一下上图:
1、一个源程序有时可能分成几个模块存放在不同的文件里,将这些源程序汇集在一起的任务由一个叫做 预处理程序 的程序来完成,有些预处理程序也负责宏展开,像C语言的预处理程序要完成文件合并、宏展开等任务
2、如果 编译程序 生成的目标程序是汇编代码形式,需要经由 汇编程序 翻译成可再装配(或可重定位)的机器代码,再经由 装配/连接编辑程序 与某些库程序连接成真正能在机器上运行的代码
也即,一个编译程序的输入可能要由一个或多个预处理程序来产生;另外,为得到能运行的机器代码,编译程序的输出可能仍需要进一步地处理
上面也介绍过,编译程序的基本任务是将源语言程序翻译成等价的目标语言程序。然而源语言的种类成千上万,从常用的诸如FORTRAN PASCAL和C/C++
语言,到各种各样的计算机应用领域的专用语言;而目标语言也是成千上万的,加上编译程序根据它们的构造不同,所执行的具体功能的差异又分成了各种类型,比如:
一趟编译、多趟编译的、具有调试或优化功能的编译等。
尽管存在这些明显的复杂因素,但是任何编译程序所必须执行的主要任务基本是一样的,通过理解这些任务,使用同样的基本技术,我们可以为各种各样的 源语言和目标语言 设计和构造编译程序。
上世纪50年代末,随着编译技术的发展和社会对编译程序需求的不断增长 有些研究人员开始研究 编译程序的自动生成工具,提出并研制 编译程序的编译程序。其功能在于:
以任一语言的词法规则、语法规则和语义解释出发,自动产生该语言的编译程序
截至目前为止,很多自动生成工具已经被广泛使用 如PostgreSQL数据库中的 词法分析程序的生成系统LEX,语法分析程序的生成系统YACC等。
编译程序的结构
下面所说的编译过程的6个阶段的任务可以分别由6个模块来完成,分别称作:
- 词法分析程序
- 语法分析程序
- 语义分析程序
- 中间代码生成程序
- 代码优化程序
- 目标代码生成程序
除此之外,编译程序的另外两个重要的工作是表格管理程序和出错处理程序。他们与上述六个阶段都有联系:
1、编译过程中源程序的各种信息被保留在种种不同的表格里,编译各阶段的工作都涉及到构造、查找或更新有关的表格,因此需要有表格管理的工作
2、如果编译过程中发现源程序有错误,编译程序应报告错误的性质和错误发生的地点,并且将错误所造成的影响限制在尽可能小的范围内,使得源程序的其余部分能继续被编译下去,有些编译程序还能自动校正错误,这些工作称之为出错处理
其中最重要的一种表格是符号表。符号表中记录源程序中使用的名字和收集到的每个名字的各种属性信息,诸如类型、作用域、分配存储信息。出错处理程序的任务包括检查错误、报告出错信息、排错、恢复编译工作。
编译过程概述
编译程序完成从源程序到目标程序的翻译工作,是一个复杂的整体的过程。从概念上来讲,一个编译程序的整个工作过程是划分成阶段进行的,每个阶段将源程序的一种表示形式转换成另一种表示形式,各个阶段进行的操作在逻辑上是紧密连接在一起的。
一般一个编译过程划分成 词法分析、语法分析、语义分析、中间代码生成,代码优化和目标代码生成 这六个阶段,如下图所示。
这是一种典型的划分方法。事实上,某些阶段可能组合在一起,这些阶段间的源程序的中间表示形式就没必要构造出来了。在前面所讨论的编译过程中阶段的划分是编译程序的逻辑组织。有时,常常把编译的过程分为前端(front end
)和后端(back end
) 。前端的工作主要依赖于源语言而与目标机无关;后端工作依赖于目标机而一般不依赖源语言,只与中间代码有关的那些阶段的工作。
通常前端包括词法分析、语法分析、语义分析和中间代码生成这些阶段,某些优化工作,即中间代码优化也可在前端做,也包括与前端每个阶段相关的出错处理工作和符号表管理等工作。后端工作包括目标代码生成和目标代码优化,以及相关出错处理和符号表操作。
若是按照组合的方式来实现编译程序,则可以有如下组合:
- 某一编译程序的前端 + 相对应的后端 → 为不同的机器构成同一个源语言的编译程序
- 不同语言编译的前端生成同一种中间语言 + 一个共同的后端 → 为同一机器生成几个不同语言的编译程序
下面,我们从源程序在不同阶段所被转换成的表示形式来介绍各个阶段的任务:
词法分析
词法分析阶段是编译过程的第一个阶段。这个阶段的任务是从左到右一个字符一个字符地读入源程序,对构成源程序的字符流进行扫描和分解,从而识别出一个个单词(也称单词符号或符号)。这里所谓的单词是指逻辑上紧密相连的一组字符,这些字符具有集体含义。
比如标识符用于表示变量名,是由字母字符开头,后跟字母、数字字符的字符序列组成的一种单词。
保留字(关键字或基本字)是一种单词,此外还有算符,界符等等。
例如某源程序片断如下:
begin var sum, first, count:real; sum∶=first+count*10 end.
词法分析阶段将构成这段程序的字符组成了如下19个单词序列:
- 保留字begin
- 保留字var
- 标识符sum
- 逗号,
- 标识符first
- 逗号,
- 标识符count
- 冒号:
- 保留字real
- 分号;
- 标识符sum
- 赋值号∶=
- 标识符first
- 加号+
- 标识符count
- 乘号*
- 整数10
- 保留字end
- 界符.
从上面可以看出,五个字符即b e g i n
构成了一个称为保留字的单词begin,两个字符即∶和=构成了表示赋值运算的符号∶=
。这些单词间的空格在词法分析阶段都被滤掉了。我们知道,标识符用于表示变量名,可以很方便的使用id1,id2和id3分别表示sum,first和count三个标识符的内部形式,那么经过词法分析后上述程序片断中的赋值语句
sum∶=first+count*10
则表示为
id1∶=id2+id3*10
或者一个C源程序片断:
int a;
a = a + 2;
词法分析后的结果可能返回(从左到右 从上到下):
单词类型 | 单词值 |
---|---|
保留字 | int |
标识符 | a |
界符 | ; |
标识符 | a |
算符(赋值) | = |
标识符 | a |
算符(加) | + |
整数 | 2 |
界符 | ; |
词法分析有关的专业术语:
- 词法分析—lexical analysis 或者scanning
- 单词—token
- 保留字—resered word
- 标识符 —identifier(user-defined name)
语法分析
语法分析是编译过程的第二个阶段。语法分析的任务是在词法分析的基础上将单词序列分解成各类语法短语,如"程序",“语句”,"表达式"等。一般这种语法短语,也称语法单位可表示成语法树,比如上述程序段中的单词序列:
id1∶=id2+id3*10
或者
sum∶=first+count*10
经语法分析得知其是PASCAL语言的"赋值语句",表示成如下图所示的语法树:
语法分析所依据的是:语言的语法规则,即 描述程序结构的规则。我们这里通过语法分析来确定如上图所示对应的 完整输入字符串 是否可以构成一个在语法上正确的程序。
程序的结构通常是由递归规则来表示的,如下:
定义一个表达式的规则:
- 任何标识符是表达式
- 若表达式1 表达式2都是表达式,则:
表达式1 +、*、-、/ 表达式2
也是表达式;(表达式1)
也是表达式
定义一个语句的递归规则:
- 标识符 := 表达式 是语句
while(表达式) do 语句
和if(表达式) then 语句 else 语句
都是语句
OK,我们这里重新回到上图,上面的赋值语句:
sum∶=first+count*10
就可以根据 赋值语句和表达式的递归定义规则进行展开为上面的语法树。
词法分析和语法分析本质上都是对源程序的结构进行分析。但词法分析的任务仅对源程序进行线性扫描即可完成,比如识别标识符,因为标识符的结构是字母打头的字母和数字串,这只要顺序扫描输入流,遇到既不是字母又不是数字字符时,将前面所发现的所有字母和数字组合在一起而构成单词标识符。但这种线性扫描则不能用于识别递归定义的语法成分,比如就无法仅用线性扫描去匹配表达式中的括号。
语义分析
语法分析是编译过程的第三个阶段。语义分析阶段的任务是审查源程序有无语义错误,以及类型审查、类型提升、为代码生成阶段收集类型信息。源程序中有些语法成分,按照语法规则去判断,它是正确的,但它不符合语义规则。比如:
- 使用了没有声明的变量
- 或者给一个过程名赋值
- 或者调用函数时参数类型不合适
- 或者参加运算的两个变量类型不匹配等。
比如下边的程序片段:
int arr[2],c;
c = arr1 * 10 ;
其中的赋值语句是符合语法规则的,但是因为没有声明变量arr1
,而存在语义错误。
语义分析的其中一个工作就是:进行类型审查,审查每个算符是否具有语言规范允许的运算对象。
- 当不符合语言规范时,编译程序应报错
但是这里面会有所不同,例如有的编译程序会对实数用作数组的下标报错,而有的因为某些语言规定了运算对象可以被强制转换数据类型。例如:
当一个二目运算 的对象是一个整型对象和一个实型对象时,编译程序会把整型对象给转换成实型对象 然后进行运算而非报错。
比如上图中的sum∶=first+count*10
,count是个实型 10则是整型,那么在语义解析阶段进行了 类型审查 之后,整型10 就会变成实型10.0 则体现在 分析树上的情形如下图所示:
注:这里增加的这个语义处理结点 inttoreal ,就是表示:整型10 就会变成实型10.0 的一目算符。
下面来小结一下语义分析主要的任务:
- 完成静态语义审查和处理
- 上下文相关性审查
- 类型匹配审查
- 类型转换
中间代码生成阶段
在进行了上述的词法分析,语法分析和语义分析阶段的工作之后,有的编译程序将源程序变成一种内部表示形式,这种内部表示形式叫做中间语言或中间代码。
所谓"中间代码"是一种结构简单、含义明确的记号系统,这种记号系统可以设计为多种多样的形式,重要的设计原则为两点:一是容易生成;二是容易将它翻译成目标代码。很多编译程序采用了一种近似"三地址指令"的"四元式"中间代码,这种四元式的形式为:
(运算符,运算对象1,运算对象2,结果)
比如源程序sum := first+count*10
生成的四元式序列如下图所示,其中ti(i=1,2,3)
是编译程序生成的临时名字,用于存放运算的中间结果的:
而上述的四元式(运算符,运算对象1,运算对象2,结果)又可以写成赋值语句的形式:
结果=运算对象1 运算符 运算对象2
比如c语言的源程序a = b * c + b * d
的四元式序列为:
t1 = b * c
t2 = b * d
t3 = t1 + t2
a = t3
在翻译分支,循环和函数调用等语句时,四元式的生成通常要比上述例子复杂些。比如源程序:
if (a <= b)
a = a – c;
c = b * c;
其译成的四元式为:
t1 = a > b
if t1 goto l
t2 = a – c
a = t2
l : t3 = b * c
c = t3
代码优化
代码优化阶段的任务是:对前阶段产生的中间代码进行变换或进行改造,目的是使生成的目标代码更为高效,即省时间和省空间。比如下图所示,相较于上图 仅剩了两个四元式而执行同样的计算:
也就是编译程序的这个阶段已经把将上图的10转换成实型数的代码化简掉了,同时因为t3仅仅用来将其值传递给id1,也可以被化简掉,这只是优化工作的两个方面,此外诸如公共子表达式的删除、强度削弱、循环优化等优化工作将在后面的学习中详细介绍。
但是需要注意的是:代码优化工作会降低编译程序的编译速度,因此编译优化阶段常常作为可选择阶段,编译程序具有控制机制以允许用户在编译速度和目标代码的质量间进行权衡。
目标代码生成
目标代码生成阶段的任务是:把中间代码变换成特定机器上的绝对指令代码或可重定位的指令代码或汇编指令代码。这是编译的最后阶段,它的工作与硬件系统结构和指令含义有关,这个阶段的工作很复杂,涉及到硬件系统功能部件的运用、机器指令的选择、各种数据类型变量的存储空间分配以及寄存器和后缓寄存器的调度等。
这里举一个例子进行详细说明,已知id1:= id2 + id3 * 10
的优化后四元式序列为:
(1) ( * id3 10.0 t1)
(2) ( + id2 t1 id1)
使用两个寄存器(R1和R2),上面的中间代码即可生成如下图的某硬件体系下的汇编代码:
解释一下:
1、第一条指令将id3的内容送至寄存器R2
2、第二条指令将其与实常数10.0相乘,这里用#表明10.0处理为常数
3、第三条指令将id2移至寄存器R1
4、第四条指令加上前面计算出的R2中的值
5、第五条指令将寄存器R1的值移到id1的地址中
这些代码实现了本节开头给的源程序片断的赋值
前面说过,上述编译过程的阶段划分是一种典型的处理模式,事实上并非所有的编译程序都包括这样几个阶段。有些编译程序并不需要生成中间代码,即不存在中间代码生成阶段;有些编译程序不进行优化,优化阶段即可省去;而有些最简单的编译程序只有词法分析,语法分析,语义分析和目标代码生成(在语法分析的同时 产生目标指令代码)。不过多数使用的编译程序都包含上述的几个阶段的工作。
注:一个编译过程可由一遍、两遍或多遍完成。这里所谓的"遍",也称作"趟",是对源程序或其等价的中间语言程序从头到尾扫视并完成规定任务的过程。每一遍扫视可完成上述一个阶段或多个阶段的工作。例如一遍可以只完成词法分析工作;一遍完成词法分析和语法分析工作;甚至一遍完成整个编译工作。对于多遍的编译程序,第一遍的输入是用户书写的源程序,最后一遍的输出是目标语言程序,其余是上一遍的输出为下一遍的输入。
在实际的编译系统的设计中,编译的几个阶段的工作究竟应该怎样组合,即编译程序究竟分成几遍,参考的因素主要是源语言和机器(目标机)的特征。
比如源语言的结构直接影响编译的遍的划分;像PL/1或ALGOL 68 那样的语言,允许名字的说明出现在名字的使用之后,那么在看到名字之前是不便为包含该名字的表达式生成代码的,这种语言的编译程序至少分成两遍才容易生成代码。另外机器的情况,即编译程序工作的环境也影响编译程序的遍数的划分。遍数多一点,整个编译程序的逻辑结构可能清晰些,但遍数多即意味着增加读写中间文件的次数,势必消耗较多时间,一般会比一遍的编译程序要慢(但是一个遍数多的编译程序可以比少的编译程序更少占内存)。
什么是解释程序
为了实现在一个计算机上运行高级语言的程序,主要有两个途径:第一个途径是把该程序翻译为这个计算机的指令代码序列,这就是我们上面已经描述过的编译过程;第二个途径是编写一个程序,它解释所遇到的高级语言程序中的语句并且完成这些语句的动作,这样的程序就叫解释程序(下面详细说明)。
在上面的学习中可知 编译程序是一个语言处理程序:把一个高级语言程序翻译成某个机器的汇编语言程序 或者 二进制代码程序,该二进制代码程序在机器上运行以生成结果。
于是借助于编译程序,一个事先准备好在该机器上运行的高级语言程序 通过编译等操作,之后生成的程序将会以机器的速度运行。但是这里需要注意的是:不把整个程序全部翻译完成,该程序是不能开始运行的 当然也不能产生任何结果的。也即:在这里 编译和运行是两个独立分开的阶段。
但是在这样的场景下(交互式环境下),并不需要将这两个阶段给分隔开。于是新的语言处理程序:解释程序 应运而生!它不需要在运行前先把源程序翻译成目标代码,也可以直接 实现在某台机器上运行程序并产生结果。
从功能上说,一个解释程序能让计算机执行高级语言。它与编译程序的主要不同是:它不生成目标代码,它每遇到一个语句,就要对这个语句进行分析以决定语句的含义,并执行相应的动作。换言之:它接受某个语言的程序并立即运行该源程序。其工作模式如下:
- 一个个的获取、分析并执行源程序语句
- 一旦第一个语句分析结束,源程序便开始运行并产生结果
- 接下来 next行
从上面可以看出,这种解释程序特别适合程序开发者以 交互式下 的工作,即:希望在获取下一个语句之前了解每个语句的执行结果,然后允许执行时修改程序。
下面来看一个简单的实例进而展示两种程序的实质性区别:
一、从中间代码生成上来看:
b:=2;
a:=b+2;
write a;
解释程序直接输出结果 4。而编译程序则要生成目标代码,如下:
movf #2.0, b
movf b , R1
addf #2.0, R1
movf R1, a
编译系统生成的目标代码由计算机执行才能生成结果。使用编译系统时会区分编译阶段和运行阶段,编译阶段对源程序进行编译,运行阶段是指目标程序的运行。而解释系统则是边解释边执行,运行目标程序时的控制权在解释器而不在目标程序。
二、从存储结构组织来看:
对编译系统而言:在编译阶段,存储区一般要有源程序缓冲区,目标代码缓冲区,名字表以及编译程序使用的源程序中间表示和各种表格等。在目标代码运行阶段,存储区只有目标代码和数据区了(编译所用的任何信息都不再需要了)。
对解释系统来说:在它工作的自始至终,存储区中要有源程序,名字表,标号表等表格,输入输出缓冲区以及数据区等。毕竟解释程序一般是要把 源程序的每一个语句 单独进行语法分析,转换成一种内部表示形式(存放在源程序区)
注:也正是因为解释程序 允许在执行用户程序时修改用户程序,所以这就要求 在解释程序工作的整个过程中,源程序 符号表等内容始终存放在存储区中,并且存放格式要设计的易于使用和修改。
接下来对这两种程序的异同点进行小结:
翻译:是按源程序的实际输入顺序,处理程序语句,得到执行的目标程序
解释:是按源语言的定义边解释边执行,解释执行是按照被解释的源程序逻辑流程进行工作的
编译器:工作效率高,即时间快 空间开销小;交互性与动态性差,可移植性差
解释器:工作效率低,即时间慢 空间开销大;交互性与动态性好,可移植性好(因对源程序的循环语句部分要反复解释执行)
共同点:均完成对源程序的翻译
不同点:编译器采用先翻译后执行,解释器采用边翻译边执行;运行目标程序时的控制权不同
著名的解释程序有:
- Unix命令语言解释程序 shell
- 数据库查询语言SQL解释程序
- Basic Lisp等语言解释程序
- Java语言环境的ByteCode解释程序
- …