一个很笨很笨的人的编译自救笔记。
1 程序设计语言
程序设计语言用于书写计算机程序的语言。语言的基础是一组记号和一组规则。根据规则由记号构成的记号串的总体就是语言。在程序设计语言中,这些记号串就是程序。
程序设计语言由三个方面的因素,语法、语义和语用。
- 语法表示程序的结构或形式,即表示构成语言的各个记号之间的组合规律,但不涉及这些记号的特定含义,也不涉及使用者。
- 语义表示程序的含义,即表示按照各种方法所表示的各个记号的特定含义,但不涉及使用者。
1.1 基本概念
源程序:用汇编语言或高级语言编写的程序称为源程序。
目标程序:用目标语言所表示的程序。
翻译程序:将源程序转换为目标程序的程序称为翻译程序。
汇编程序:若源程序用汇编语言书写,经过翻译程序得到用机器语言表示的程序,这是的翻译程序就称之为汇编程序。
编译程序:若源程序是用高级语言书写,经加工得到目标程序,这种翻译过程称为编译。
1.2 编译过程
1.2.1 词法分析
任务:分析和识别单词。
单词:是语言的基本语法单位。
1.2.2 语法分析
任务:根据语法规则(即语言的文法),分析并识别出各种语法成分,如表达式、各种说明、各种语句、过程、函数等,并进行语法正确性检查。
1.2.3 语义分析、生成中间代码
任务:对识别出的各种语法成分进行语义分析,并产生相应的中间代码。
中间代码:一种介于源语言和目标语言的中间语言形式。
目的:
- 便于做优化处理;
- 便于编译程序的移植;
中间代码的形式:常用四元式、三元式、逆波兰表示。
四元式(三地址指令)
1.2.4 代码优化
任务:目的是为了得到高质量的目标程序。
1.2.5 生成目标程序
由中间代码很容易生成目标程序。这部分工作与机器关系密切,所以要根据机器进行。在做这部分工作时(要注意充分利用累加器),也可以进行优化处理。
1.2.6 编译程序构造
上述五个阶段都需要做两件事:建表查表,出错处理。
符号表管理:在整个编译过程中始终都要贯穿着建表(填表)和查表的工作。即要及时地把源程序中的信息和编译过程中所产生的信息登记在表格中,而在随后的编译过程中同时又要不断地查找这些表格中的信息。
前端:与源程序有关的编译部分称为全段。
后端:与目标机有关的部分称为后端。
2 文法和语言的概念和表示
2.1 形式语言
字母表:符号的非空有限集;
符号:字母表中的元素;
符号串:符号的有穷序列;
空符号串:无任何符号的符号串。
符号串的闭包运算:
2.2 文法的非形式讨论
文法是对语言结构的定义与描述。即从形式上用于描述和规定语言结构的称为文法。
语法规则:我们通过建立一组规则,来描述句子的语法结构。
由规则推导句子:有了一组规则之后,可以按照一定的方式用它们去推导或产生句子。
推导方法:从一个要识别的符号开始推导,即用相应规则的右部来替代规则的左部,每次仅用一条规则去进行推导。
所谓文法是在形式上对句子结构的定义和描述,而未涉及语义问题。
语法树:我们用语法树来表述一个句子的语法结构。
2.3 文法和语言的形式定义
当符号串已没有非终结符号时,推导就必须终止。因为终结符不可能出现在规则左部,所以将在规则左部出现的符号称为非终结符号。
规范推导=最右推导
2.3.1 递归文法
递归规则:
递归文法:
递归文法的优点:可用有穷条规则,定义无穷语言。
左递归文法的缺点:不能用自顶向下的方法来进行语法分析。
2.3.2 一些概念
短语是前面句型中某个非终结符所能推出的符号串。
短语、简单短语、句柄都是基于句型来说的,先画出语法树,一个结点的子节点就是他的短语,一个结点能推出的终结符就是他的简单短语,在整棵子树最左边的终结符就是句柄。一个句型可能有多个短语、简单短语,而句柄只有一个。
2.4 语法树与二义性文法
语法树:句子结构的图示表示法,它是有向图,由结点和有向边组成。
文法所能产生的句子,可用用不同的推导序列(使用产生式顺序不同)将其推导出来。语法树的生长规律不同,蛋最终生成的语法树形状完全相同。
子树:语法树中的某个结点(子树的根)连同它向下派生的部分所组成。
文法的二义性:若对一个文法的某一句子,存在两颗不同的语法树,则该文法是二义性文法,否则是无二义性文法。
文法的二义性意味着句型的句柄不唯一。
若一个文法的某规范句型的句柄不唯一,则该文法是二义性的,否则是无二义性的。
若文法是二义性的,则在编译时就会产生不确定性,遗憾的是在理论上已经证明:文法的二义性是不可判定的,即不可能构造出一个算法,通过有限步骤来判定任一文法是否有二义性。
多余规则:
- 在推导文法的所有句子中,始终用不到的规则,即该规则的左部非终结符不出现在任何句型中。(不可达符号)
- 在推导句子的过程中,一旦使用了该规则,将退不出任何终结符号串。即该规则中含有退不出任何终结符号串的非终结符。(不活动符号)
2.5 文法的其他表示法
扩充的BNF表示
语法图
2.6 文法和语言分类
形式语言:用文法和自动机所描述的没有语义的语言。
文法定义:
语言定义:
文法和语言分类:0型、1型、2型、3型
0型
0型文法称为短语结构文法。规则的左部和右部都可以是符号串,一个短语可以产生另一个短语。
可以用图灵机接受。
1型
1型称为上下文敏感或上下文有关。即只有在x、y这样的上下文中才能把U改写为u。
可以用一种线性界限自动机接受。
2型
2型文法称为上下文无关文法,即把U改写为u时,不必考虑上下文。
2型文法与BNF等价。
2型语言可以由下推自动机接受。
3型
3型文法称为正则文法。它是对2型文法进行进一步限制。
又称正则语言、正则集合,这种语言可以被有穷自动机接受。
0型文法可以产生L0,L1,L2,L3
但2型文法只能产生L2,L3不能产生L0,L1。
3型文法只能产生L3。
3 词法分析
3.1 词法分析程序的功能及实现方案
词法分析:
- 根据词法规则识别及组合单词,进行词法检查。
- 对数字常数完成数字字符串到二进制数值的转换。
- 删去空格字符和注释。
3.2 单词种类及输出形式
几种常用的单词内部形式:
- 按单词种类分类
- 保留字和分界符采用一符一类
- 标识符和阐述的单词值又为指示字
3.3 正则文法和状态图
左线性文法的状态图的画法:
4 语法分析
4.1 语法分析概述
功能:根据文法规则,从源程序单词符号串中识别出语法成分,并进行语法检查。
基本任务:识别符号串S是否为某语法成分。
- 自顶向下分析
- 自底向上分析
自顶向下分析:
自底向上分析算法:
4.2 自顶向下分析
给定符号串S,若预测某一语法成分,则可根据该语法从成分的文法,设法S构造一个语法树。
自顶向下分析方法特点:
- 分析过程时带预测的,对输入符号串要预测属于说明语法成分,然后根据该语法成分的文法建立语法树。
- 分析过程是一种试探过程,是尽一切办法来建立语法树的过程,由于是试探过程,难免又失败,所以分析过程需进行回溯,因此也称这种方法是带回溯的自顶向下分析方法。
- 最左推导可以编写程序来实现,但带回溯的自顶向下分析方法在实际上价值不大,效率低。
自顶向下分析的基本缺点是:不能处理具有左递归性的文法。
如果在匹配输入串的过程中,假定正好轮到要用非终结符U直接匹配输入串,即要用非终结符U直接匹配输入串,即用U的右部符号串去匹配,为了用U去匹配,又得用U去匹配,这样无限的循环下去将无法终止。
如果文法具有间接左递归,则也将发生上述问题,只不过环的圈子都得更大。
要实行自顶向下分析,必须要消除文法的左递归。
消除直接左递归
用扩充的BNF表示来改写文法。
改写文法消除左递归,又前后等价:
- 提因子
- 将左递归规则改为右递归规则
具有一个直接左递归的右部并位于最后,这表明该语法类U是由x或y或z气候随有0个v或多个v组成。
消除一般左递归
一般左递归也可以通过改写文法予以消除。
消除所有左递归的算法:
消除回溯的途径
- 改写文法:对具有多个右部的规则反复提取左因子;
- 超前扫描:当文法不满足避免回溯的条件时,即各个选择的首付好相交时,可以采用超前扫描的方法,即向前侦察个输入符号串的第二个第三个符号来确定要选择的目标。
为了在不采用超前扫描的前提下实现不带回溯的自顶向下分析,文法需要满足两个条件:
- 文法是左递归的;
- 对文法的任意非终结符,若其规则右部有多个选择时,各个选择所推出的终结符号串的首符号集合要两两不相交。
递归子程序法
对语法的每一个非终结符都编一个分析程序,当根据文法和当时的输入符号预测到要用某个非终结符去匹配输入串时,就调用该非终结符的分析程序。
- 检查并改写文法:改写后无左递归且首符集不相交;
- 检查文法的递归性;
5 符号表管理技术
5.1 概述
符号表:在编译过程中,编译程序用来记录源程序中各种名字的特性信息,多以也称为名字特性表。
名字:程序名、过程名、函数名、用户定义类型名、变量名、常量名、枚举值名、标号名。
特性信息:上述名字的种类、类型、位数、参数个数、数值及目标地址等。
5.2 符号表的组织与内容
- 统一符号表:不管上面名字都填入统一格式的符号表中。
- 对于不同种类的名字分别建立各种符号表。
- 折中办法:大部分共同信息组成统一格式的符号表,特殊信息另设附表,两者用指针链接。
5.3 栈式符号表结构
当过程和函数体编译完成后,应将与之相应的参数名和局部变量名以及后者的特性信息从符号表中删去。
6 运行时的存储组织及管理
6.1 概述
目标程序运行时所需存储空间的组织与管理以及源程序中变量存储空间的分配。
静态存储分配:在编译阶段由编译程序实现对存储空间的管理和为源程序中的变量分配存储的方法。
- 如果在编译时能够确定源程序中变量在运行时的数据空间大小,且运行时不改变没那么就可以采用静态存储分配方法。
动态存储分配:在目标程序运行阶段由目标程序实现对存储空间的组织与管理,和为源程序中的变量分配存储的方法。
- 在目标程序运行时进行变量的分配。
- 编译时要生成进行动态分配的指令。
6.2 静态存储分配
分配策略:由于每个变量所需空间的大小在编译时已知,因此可以用简单的方法给变量分配目标地址。
- 开辟一数据区(首地址在加载时定)
- 按编译顺序给每个模块分配存储空间
- 在模块内部按顺序给模块的变量分配存储,一般用相对地址没所占数据区的大小由变量类型决定。
- 目标地址填入变量的符号表中。
6.3 动态存储分配
分配策略:整个数据区为一个堆栈;当进入一个过程时,在栈顶为其分配一个数据区;退出时,撤销过程数据区。
6.3.1 活动记录
一个典型的活动记录可以分为三部分:
- 局部数据区:存放模块中定义的各个局部变量;
- 参数区:存放隐式参数和显式参数;
- display区:存放各外层模块活动记录的基地址;
参数区:
prev abp:存放调用模块记录基地址,函数执行完时,释放其数据区,数据区指针指向调用前的位置。
ret addr:返回地址,即调用语句的吓一跳执行指令地址;
ret value:函数返回值;
形参数据区:每一形参都要分配数据空间,形参单元中存放实参值或者实参地址。
display区:
6.3.2 构造display区的规则
- 如果j在i的高层:从i层模块进入j层模块:复制i层的dispaly,然后增加一个指向i层模块记录及地址的指针。
- 如果j <= i 即调用外层模块或同层模块:将 i 层模块的dispaly区中的前面 j - 1 各入口复制到第j层模块的display区。
6.3.3 运行时的地址计算
6.# C语言运行时存储管理
- 栈区:向下增长;保存局部变量;
- 堆区:向上增长;保存由malloc系列函数或new操作符分配的内存;
- 静态区:未初始化全局变量;已初始化全局变量、静态变量、常量;
- 代码区:可执行代码;
栈式分配和堆式分配的比较
栈 | 堆 |
解决了函数递归调用等问题 | 解决了动态申请空间的问题 |
由编译器自动管理 | 由程序员控制空间的申请和释放操作 |
向内存地址减少的方向增长 | 向内存地址增加的方向增长 |
不会产生碎片 | 会产生碎片 |
计算机底层支持,分配效率高 | C函数库支持,分配效率低 |
7 源程序的中间形式
一般编译程序都生成中间代码,然后再生成目标代码,主要优点是可移植(与具体目标程序无关)。
7.1 波兰表示
前缀表达(波兰表达)
后缀表达(逆波兰表达)
算法:设一个操作符栈,当读到操作数时,立即输出改操作数,当扫描到操作符时,与栈顶操作符比较优先级,若栈顶操作符优先级高于栈外,则输出该栈顶操作符,反之,则栈外操作符入栈。
转换算法:
波兰表达式的优点:
- 在不使用括号的情况下可以无二义地说明算数表达式;
- 波兰表示法更容易转换成机器的汇编语言或机器语言;
- 波兰表达不仅能用来作为算数表达式的中间代码形式,而且也能作为其他语言结构的中间代码形式;
7.2 N-元表示
在该表示中,每条指令由n个域组成,通常第一个域表示操作符,其余为操作数。
常用的n元表示:三元式四元式;
三元式:
间接三元式:将执行顺序和三元式编号分离。
四元式:
7.3 中间代码的图结构表示
抽线语法树:用树型图的方式表示中间代码,操作数出现在叶结点上,操作符出现在中间节点。
DAG图:有向无环图,语法树的一种规约表达形式。
7.4 一种特殊的四元式表达式:SSA
静态单一复制形式的IR主要特征是每个变量只赋值一次。
SSA优点:
- 可以简化很多优化的过程
- 可以获得更好的优化结果
8 错误处理
8.1 错误分类
从编译角度,将错误分为两类:语法错误和语义错误。
语义错误:源程序在语法上不合乎文法。
语义错误主要包括:程序不符合语义规则或超越具体计算机系统的限制。
语义规则:
- 标识符先说明后引用;
- 标识符引用要符合作用域规定;
- 过程调用时实参要与形参一致;
- 参与运算的操作数类型一致;
- 下标变量下标不能越界;
超越系统限制:
- 数据溢出错误;
- 符号表、静态存储分配数据区溢出;
- 动态存储分配数据区溢出;
8.2 错误的诊察和报告
错误诊察:
- 违反语法和语义规则以及超过编译系统限制的错误。
- 下标越界,计算结果溢出以及动态存储数据区溢出。
错误报告:
- 出错位置;
- 出错性质;
8.3 错误处理技术
发现错误后,在报告错误的同时还要对错误进行处理,以方便编译能进行下去。
- 错误改正:根据文法进行错误改正;
- 错误局部化处理:当发现错误后,尽可能把错误影响限制在一个局部的范围,避免错误扩散和应先程序其他部分的分析。
一般原则:当诊察到错误以后,就暂停对对面符号的复习,跳过错误所在的语法成分然后继续向下分析。
错误局部化处理的实现(递归下降分析法):
- 用递归下降分析时,如果发现错误,便将有关错误信息送cx,然后转出错误处理程序;
- 出错程序先打印或显示出错位置以及出错信息,然后跳出一段源程序,知道跳到语句的右界符或正在分析的语法成分的合法后继符号为止,然后再往下分析。
9 语法制导翻译技术
词法分析,语法分析:解决单词和语言成分的识别及词法和语法结构的检查。语法结构课形式化地用一组产生式来描述。给定一组产生式,能够狠容易地将其分析器构造出来。
9.1 翻译文法和语法制导翻译
输入文法:未插入动过符号时的文法。由输入文法可以通过推导产生输入序列。
翻译文法:插入动作符号的文法。由翻译文法可以通过推导产生活动序列。
活动序列:由翻译文法推导出的符号串,由终结符和动作符号组成。
- 从活动序列中,抽出动作符号,则得输入序列;
- 从活动序列中,抽取输入序列,则得动作序列;
翻译文法是上下文无关文法,其终结符号集由输入符号和动作符号组成。由翻译文法所产生的终结符号串称为活动序列。
符号串翻译文法:若插入文法中动作符号对应的语义子程序是输出动作符号标记@后的字符串的文法。
语法知道翻译:按翻译文法进行的翻译。
给定一输入符号串,根据翻译文法获得翻译该符号串的动作序列,并执行该序列所规定的动作的过程。
9.2 属性翻译文法
在翻译文法的基础上,可以进一步定义属性文法,翻译文法中的符号,包括终结符、非终结符和动作符号均可带有属性,这样能更好的描述和实现编译过程。
9.2.1 L-属性翻译文法
这是属性翻译文法中较简单的一种,其输入文法要求是LL(1)文法,可用自顶向下分析构造分析器。在分析过程中可进行属性求值。
L-属性翻译文法事带有下列说明的翻译文法:
- 文法中的终结符,非终结符及动作符号都带有属性,且每个属性都有一个值域。
- 非终结符及动作符号的属性可分为继承属性和综合属性。
- 开始符号的继承属性具有指定的初始值。
- 输入符号的每个综合属性具有指定的初始值。
属性的求值规则:
继承属性:
- 产生式左部非终结符号的继承属性值,取前面产生式右部该符号已有的继承属性值。
- 产生式右部符号的继承属性值,用该产生式左部符号的继承属性或出现在该符号左部的符号属性值进行计算
综合属性(适合在滴定向下分析过程中求值):
- 产生式右部非中介符号的综合属性值,取其下部产生式左部同名非终结符号的综合属性值。
- 产生式左部非终结符号的综合属性值,用该产生式左部符号的继承属性或某个右部符号的属性进行计算。
- 动作符号的综合属性用该符号的继承属性或某个右部符号的属性进行计算。
一个L-ATG被定义为简单赋值形式的(SL-ATG),当且仅当满足如下条件:
- 产生式右部符号的继承属性是一个常量,它等于左部符号的继承属性值或等于出现在所给符号左边符号的一个综合属性值。
- 产生式左部非终结符号的综合属性是一个常量,它等于左部符号的继承属性值或等于右部符号的综合属性值。
9.3 自顶向下语法制导翻译
9.3.1 翻译文法的自顶向下翻译-递归下降翻译器
按翻译要求,在文法中插入语义动作符号,在分析过程中调用相应语义处理程序,完成翻译任务。
9.3.2 属性文法自顶向下翻译的实现-递归下降翻译器
对于每个非终结符号都编写一个翻译子程序。根据该非终结符号具有的属性数目,设置相应的参数。
继承属性:声明为复制形参。
综合属性:声明为变量形参。
10 词法自动化
10.1 正则文法和状态图
左线性文法的状态图的画法:
- 令G的每个非终结符都是一个状态;
- 设一个开始状态S;
- 若Q ::= T, Q ∈ T, T∈ Vt,见图1;
- 若Q ::= RT, Q, R ∈ Vn, T∈ Vn,见图2;
- 按自动机方法,可加上开始状态和终止状态标志;
识别算法:
- 置初始状态为当前状态,从x的最左字符开始,重复步骤2,直到x右端为止。
- 扫描x的下一个字符,在当前状态所射出的弧中找出标记有该字符的弧,那么x不是句子,过程到此结束;如果扫描的是x最右端的字符,并从当前状态出发沿着标有该字符的弧过渡到下一个状态为终止状态Z,则x是句子。
11 正则表达式
11.1 正则表达式
正则表达式:
正则表达式中的运算符:
| 或; · 连接; * 或 {} 重复; ()括号;
a*表示由任意个a组成的串;
而{a,b}* = {e,a,b,aa,ab,ba,bb...}
运算符优先级:
先*,后·,最后|。
正则表达式相等等价于这两个正则表达式表示的语言相等。
正则表达式与3型文法等价
11.2 有穷自动机
11.2.1 确定的有穷自动机(DFA)-状态图的形式化
11.2.2 不确定的有穷自动机(NFA)
若是一个多值函数,且输入可允许为空字符串,则有穷自动机是不确定的,即在某个状态下,对于某个输入字符存在多个后继状态。
从同一状态出发,有同一字符标记的多条边,或者有以空字符标记的特殊边的自动机。
11.2.3 NFA的确定化
不确定的有穷自动机与确定的有穷自动机从功能上来说是等价的。
集合I的e-闭包:
令I是一个状态集的子集,定义e-closure(I)为:
- 若s∈I,则s∈s-closure(I);
- 若s∈I,则s出发经过任意条e弧能够到达任何状态都属于s-closure(I)。
令I是NFA M’的状态集的一个子集,a∈∑,定义:Ia = e-closure(J),其中J = (s,a)
11.2.4 DFA的最简化(最小化)
一个有穷自动机是化简的等价于它没有多余状态并且他的状态中没有两个是互相等价的。
一个有穷自动机可以通过消除多余状态和合并等价状态进而转换成一个最小的与之等价的有穷自动机。
有穷自动机的多余状态:从该自动机的开始状态出发,任何输入串也不能到达那个状态。
等价状态:
- 一致性条件:状态s和t必须同时为可接受状态或不可接受状态;
- 蔓延条件:对于所有输入符号,状态s和t必须转换到等价的状态里;
分割法:把一个DFA的状态分割成一些不相关的子集,是的任何不同的两个子集状态都是可区别的,而同一个子集中的任何状态都是等价的。
12 语义分析和代码生成
12.1 语义分析
用上下文无关文法只能描述语言的语法结构,而不能描述其语义。
12.2 栈式抽象机及其汇编指令
栈式抽象机:由三个存储器、一个指令寄存器和多个地址寄存器组成。
存储器:数据存储器(存放AR的运行栈)、操作存储器(操作数栈)、指令存储器。
12.3 声明处理
编译程序处理声明语句要完成的主要任务为:
- 分离出每一个被声明的实体,并把它们的名字填入符号表中;
- 把被声明实体的有关特性信息尽可能多地填入符号表中;
13 代码优化
基本块:
- 基本块中的代码时连续的语句序列
- 程序的执行只能从基本块的第一条语句进入
- 程序的执行只能从基本块的最后一条语句离开
划分基本块:
- 输入:四元式序列
- 输出:基本块列表,每个四元式仅出现在一个基本块中;
方法:
- 首先确定入口语句的集合:①整个语句序列的第一条语句属于入口语句;②任何能由条件/无条件转移到的第一条语句属于入口语句③紧跟在跳转语句之后的第一条语句输入入口语句;
- 每个入口语句直到下一个入口语句,或者程序结束,它们之间的所有语句属于同一个基本块;
13.1 块内优化
(1)利用代数性质
编译时完成常量表达式的计算,整数类型与实型的转换;
下标变量引用时,其地址计算的一部分工作可在编译时预先做好;
用一种需要较少执行时间的运算代替另一种运算
(2)常数合并和传播
如x:=y这样的赋值语句称为复写语句。由于x和y值相同,所以当满足一定条件时,在该赋值语句下面出现的x可用y来代替。
(3)删除冗余代码
冗余代码就是毫无实际意义的代码,又称死代码(dead code)或无用代码(useless code)。
消除公共子表达式:
DAG图(有向无环图)用来表示基本块内各中间代码之间的关系。
方法:
- 首先建立节点表,该表记录了变量名和常量值,以及它们当前所对应的DAG图所对应的DAG图中结点的序号。该表初始状态为空。
- 从第一条中间代码开始,按照以下规则建立DAG图。
- 对于形如z = x op y的中间代码,其中z为记录计算结果的变量名,x为左操作数,y为右操作数,op为操作符:首先在节点表中寻找x,如果找到,记录下x当前所对应的节点号i;如果未找到,在DAG图中新建一个叶节点,假设其节点号仍为i,标记为x(如x为变量名,该标记更改为x0);在节点表中增加新的一项(x,i),表明二者之间的对应关系。右操作数y与x同理,假设其对应节点号为j。
- 在DAG图中寻找中间节点,其标记为op,且其左操作数节点号为i,右操作数结点为j。如果找到,记录下其节点号k;如果未找到,在DAG图中新建一个中间结点,假设其节点号仍为k,并将结点i和j分别与k相连,作为其左子节点和右子节点。
- 在节点表中寻找z,如果找到,将z所对应的节点号更改为k;如果未找到,在节点表中新建一项(z,k)表明二者之间的对应关系。
- 对输入的中间代码序列一次重复上述步骤3~5.
13.2 全局优化
数据流分析
后面懒得写了