编译原理
龙书8章学习总结,略过了一些算法
第一章引论和第二章简单的语法制导翻译器
一个源程序到目标程序
源程序通过预处理器处理到编译器形成目标汇编程序到汇编器形成可重定位机器代码通过链接器/加载器变成目标机器代码
预处理器:展开所有宏定义,处理含有#部分的代码,还有删除所有的注释//和/* */。
编译:进行代码的优化还有符号的汇总(符号表)以及语义分析 语法分析 词义分析及优化后生成相应的汇编代码文件。
汇编:将指令转成当地操作系统的机器码;生成可重定向的目标文件:x86体系下的.obj 和unix下的.o文件
链接过程:
将第一个步骤所生成的.o文件里的段整合在一起,进行【符号解析】,符号解析指的是将每个符号引用刚好和一个符号定义联系起来。对符号表里的未定义的符号找到其定义的地方,代码段中,对所有指令未有其定义的符号都将其变为正确的地址例如extern行的代码以及main函数中的printf。解析正确后,再进行【符号重定位】,对其分配相应的虚拟地址。所有符号都拥有其正确的虚拟地址后,然后生成最终可执行的.exe文件,其运行的时候只需要代码段和数据段。
编译器的结构
分析
分析代码的语法语义的构成,收集有关信息放入符号表,前端
综合
根据中间表示和符号表中的信息构造目标程序,后端
步骤
词法分析
会根据字符流生成词素序列,并将有关信息放入符号表条目
2.语法分析
构造相应的语法数
3.语义分析
根据语法树和符号表中的信息来检查源程序是否和语言定义的语义一致也会收集类型信息以便中间代码生成
4.中间代码
生成一种能够被轻松翻译成为目标机器上的语言
5.代码优化
机器无关的代码优化改进中间代码,以便生成更好的目标代码
6.代码生成
以源程序的中间表示形式作为输入映射到目标语言
7.符号表管理
记录源程序中使用的变量的名字,并收集和每个名字的各种属性相关的信息
8.将多个步骤组合成趟
前端步骤1-4位一趟,代码优化为可选趟,代码生成为后端趟
编译器的前端
源程序 -> 词法分析器-> 词法单元-> 语法分析器 -> 语法分析树-> 中间代码生成器-> 三地址代码
词法分析器、语法分析器、中间代码生成器都依赖于符号表
词法分析器使得翻译器可以处理由多个字符组成的构造,比如标识符。标识符由多个字符组成,在语法分析阶段当作单元称为词法单元。
语法分析器会生成抽象语法树,它表示了源程序的层次化语法结构
中间代码生成器会将语法树进一步翻译为三地址代码吗如,x = y op z
第三章词法分析
词法分析是编译的第一阶段。主要任务是读入源程序的输入字符、将它们组成词素,生成并输出一个词法单元序列,每个词法单元对应于一个词素。词法单元序列被输出到语法分析器进行语法分析。词法分析器还要和符号表进行交互。当词法分析器发现了一个标识符词素时,它要将这个词素添加到符号表中
除了识别词素之外的其他任务之一是过滤掉源程序中的注释和空白(空格、换行符、制表符以及在输入中用于分隔词法单元的其他字符)另一个任务是将编译器生成的错误消息与源程序的位置联系起来。
相关术语
词法单元
一个词法单元名和一个可选的属性值组成。理解为对一个单词也就是词素的描述,也可以称之为总称。比如所有数字的词法单元都叫number,<=,!=等叫comparison,else就是else
模式
描述了一个词法单元的词素可能具有的形式
词素
源程序中国的一个字符序列
词法单元的规约
通过一定的正则定义匹配相应的模式识别标识符关键字保留字,消除空白符
状态转换图
状态转换图有一组被称为”状态“的结点。词法分析器扫描输入串的过程中寻找和某个模式的匹配的词素,而转换图中的每个状态代表一个可能在这个过程中出现的情况。
处理保留字和标识符:
初始化时就将各个保留字填入符号表中
为每个关键字建立单独的状态转换图
有穷自动机
不确定的有穷自动机:对其边上的标号没有任何限制,一个符号标记离开同一状态的多条边
确定的有穷自动机:有且只有一条离开该状态、以该符号为标号的边
任务
剔除空白和注释
允许词法单元之间出现任意数量的空白,还有程序中会出现注释当词法分析器消除了,语法分析器就不需要再考虑了
预读
在读取字符流的时候需要判断下一个字符是不是与当前字符需要组合,比如读了>还需要读下一个是否是=构成>=
常量
表达式中出现的整形常量可以创建一个代表整形常量的终结符号入<NUM,31>元组代表有整型常量31
识别关键字和标识符
程序设计语言中都有它相应的固定字符串组成的关键字,字符串还可以标识符来作为变量、数组、函数等命名会将其当做终结符处理
第四章语法分析
语法分析器从词法分析器获得一个由词法单元组成的串,并验证这个串可以由源语言的文法生成
上下文无关法
一个有穷的非终结符(或变元)的集合;一个有穷的终结符的集合;一个有穷的产生式集合;一个起始非终结符(变元)。
不去判断作用域等问题形成一个AST
递归下降算法
递归下降算法其实很简单,它的基本思路就是按照语法规则去匹配 Token 串。
对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式。如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回退回来,尝试另一个产生式。
程序可能遇到不同层次的错误
词法错误
包括标识符、关键字或运算符拼写错误和没有在字符串文本上正确地加上引号
语法错误
包括分号放错位置、花括号多余或缺失。
语义错误
包括运算符合运算分量之间的类型不匹配。例如void某个方法中返回某个值的return语句。
逻辑错误
可以是因程序员的错误推理而引起的任何错误
语法分析方法检测语法错误
错误恢复策略
恐慌模式的恢复
语法分析器一旦发现错误不断丢弃输入中的符号,一次丢弃一个符号,直到找到同步词法单元集合中的某个元素位置比如分号或者}
短语层次的恢复
当发现一个错误时,语法分析器可以在余下的输入上进行局部性纠正。简单来说就是对一个输入串进行增删改。
错误产生式
通过预测可能遇到的常见错误,我们可以在当前语言的文法中加入特殊的产生式。
全局纠正
在理想状态下,我们希望编译器在处理一个错误输入串时通过最少的改动将其转化为语法正确的串。
第五章语法制导的翻译
在语法分析的同时进行语义翻译叫做语法制导翻译。通过构造出的语法分析树,然后通过访问这棵树的各个结点来计算结点的属性值。
语法制导定义
是一个上下文无关法和属性及规则的结合。属性和文法符号相关联,规则和产生式相关联。
继承属性和综合属性
其实就是属性计算。需要判断AST中节点中所需的类型。通过子节点计算出来的叫S属性也就是综合属性。通过父亲节点或者兄弟节点计算出来的叫做I属性继承到的属性
SDD依赖图
依赖图描述了某个语法分析数中的属性实例之间的信息流。从一个属性实例到另一个实例的边表示计算第二个属性实例时需要第一个属性实例的值。
也就是一个表达式的值是否需要另外一个表达式求出来的值,定义了属性的求值顺序
非书中:
语义分析
控制流检查
像 return、break 和 continue 等语句,都与程序的控制流有关,它们必须符合控制流方面的规则。在 Java 这样的语言中,语义规则会规定:如果返回值不是 void,那么在退出函数体之前,一定要执行一个 return 语句,那么就要检查所有的控制流分支,是否都以 return 语句结尾。
闭包分析
很多语言都支持闭包。而要正确地使用闭包,就必须在编译期知道哪些变量是自由变量。这里的自由变量是指在本函数外面定义的变量,但被这个函数中的代码所使用。
引用消解
在高级语言里,我们会做变量、函数(或方法)和类型的声明,然后在其他地方使用它们。这个时候,我们要找到定义和使用之间的正确引用关系。
类型分析和处理
在计算机语言里,类型是数据的一个属性,它的作用是来告诉编译器或解释器,程序可以如何使用这些数据。
也就是上述的属性计算(综合属性和继承属性)
第六章中间代码生成
许多方法都可以用于中间表示。包括抽象语法树和三地址码。在给定的源语言的一个程序翻译成特定的目标机器代码的过程中,一个编译器构造出一系列中间表示。
DAG
表达式的有向无环图,是中间代码在内存中其中一种数据结构。将原本的AST树形结构中消除了冗余的子树,就是其中有可能一个变量被多次使用,消除多余的这个变量子树合成一个。
三地址代码
在三地址代码中,一条指令的右侧最多有一个运算符。三地址码是一棵语法树或一个DAG的线性表示形式。
三地址码基于两个基本概念:地址和指令。
地址可以具有如下形式:
名字。源程序名字作为三地址代码中的地址,实现中源程序名字会被替换成指定符号表条目的指针。
常量。
编译器生成的临时变量。
静态单赋值形式SSA
所有的赋值都是针对具有不同名字的变量的
SSA中有一种函数表示将一个变量不同的取值合并起来,根据上文的条件来得到其中的取值
类型的检查和翻译
类型检查
保证运算分量的类型和运算符的预期类型相匹配
翻译时的应用
根据一个名字的类型,编译器可以确定这个名字在运行时刻需要多大的存储空间
第七章运行时刻环境
编译器创建并管理一个运行时刻环境,它编译得到的目标程序就运行在这个环境中。为源程序中命名的对象分配和安排存储位置,确定目标程序访问变量时使用的机制,过程间的连接,参数传递机制,以及与操作系统、输入输出设备及其他程序的接口。
存储组织
从编译器编写者的角度来看,正在执行的目标程序在它自己的逻辑空间内运行,其中每个程序值都在这个空间中有一个地址。对这个逻辑地址空间的管理和组织是由编译器、操作系统和目标机共同完成的。
静态和动态存储分配
静态属于编译时刻,动态属于运行时刻
栈式分配。一个方法的局部名字在栈中分配空间。
堆存储。有些数据的生命周期要比创造它的某次方法调用更长,这些数据通常被分配在一个可复用存储的”堆“中。
空间的栈式分配
有些语言使用过程、函数或方法作为用户自定义动作的单元,几乎所有针对这些语言的编译器都把它们的运行时刻存储按照一个栈进行管理。方法被调用时用于存放该方法的局部变量空间被压入栈。当方法调用结束,这个空间被弹出栈。
堆管理
它被用来存储那些生命周期不确定,或者将生存到被程序显示删除为止的数据。
存储管理器
分配
当程序为一个变量或对象请求内存时,存储管管理器产生一段连续的具有被请求大小的堆空间。
回收
存储管理器把被回收的空间返还到空闲空间的缓冲池中,这样它可以复用该空间来满足其他的分配请求。
第八章代码生成
编译器的最后一个步骤就是代码生成器。以编译器前端生成的中间表示和相关符号表信息作为输入,输出语义等价的目标程序。
目标程序必须保持源程序的语义含义,还必须具有很高的质量。它必须有效地利用目标机器上的可用资源。
代码生成器三个主要任务:指令选择、寄存器分配和指派、以及指令排序。
指令选择
代码生成器必须把IR程序映射成为可以在目标机上运行的代码序列。完成这个映射的复杂性由如下的因素决定:
IR的层次、指令集体系结构本身的特性、想要达到的生成代码的质量。
寄存器分配
代码生成的关键问题之一是决定哪个值放在哪个寄存器里面。寄存器是目标机上运行速度最快的单元,但是我们通常没有足够的寄存器来存放所有的值。
求值顺序
计算执行的顺序会影响目标代码的效率。
目标代码优化
指令排序
因为有指令级别的并行所以有的时候指令重排有助于代码流水线执行的时候,其中有多条代码可以同时运行
窥孔优化
提供一个固定大小的窗口,并检查窗口内的指令,看看是否可以优化。

浙公网安备 33010602011771号