编译原理笔记整理

编译器设计者的工作不仅会影响到他们创建的编译器,还会影响到他们所创建的编译器所编译的全部程序。这种杠杆作用使得编译器设计的回报丰厚,但也使得编译器的开发工作具有挑战性。

基础

语言处理器:1. 编译器 compiler 
           2. 解释器 interpreter
1. 编译器:阅读源语言编写的程序,并把该程序翻译成为一个等价的、用目标语言编写的程序。
 - 编译器产生的机器语言目标程序通常比解释器快很多。
2. 解释器:不通过翻译的方式生成目标程序。从用户的角度看,解释器直接利用用户提供的输入执行源程序中指定的操作。
 - 解释器的错误诊断效果通常比编译器更好,解释器逐个语句执行源程序。

编译:源程序 -> 目标程序
运行目标程序:输入 -> 输出

解释:源程序 + 输入 -> 输出

Java结合了编译和解释过程:java源程序 -> 编译为字节码(中间形式,可跨平台) -> 解释执行字节码
机器语言,二进制码:
C067 0000 0002
// 把0002的值放到地址0000处
// C067 操作码
// 0000 地址
// 0002 值

引入助记符 形成汇编语言:
MOV X 2

高级语言:
X = 2
汇编:将汇编语言翻译为机器语言
编译:将高级语言翻译为汇编语言(或者机器语言)
- 分析(分解):收集有关源程序的信息,存放在符号表(symbol table)的数据结构中;符号表+中间表示形式传递给综合部分;
- 综合(构造):中间表示+符号表 -> 目标程序;
源程序翻译为目标程序的过程:
1. 预处理器(preprocessor):源程序被分割在不同的文件中,需要预处理器整合;把'宏'翻译为(替换)为定义的语句
2. 编译器(compiler):经过预处理器处理的程序,翻译为汇编语言
3. 汇编器(assambler):生成可重定位的机器语言,使用的是相对位置,可以放在内存中执行
4. 链接器(linker):解决内部外部内存地址问题。相对位置+在内存的起始位置=绝对位置,可运行的目标机器代码;链接库文件在这一步,库文件即是可重定位的程序
(附注:除了内部地址,还有外部地址,使用外部地址即可调用外部程序)
5. 加载器(loader):把所有可执行目标文件放在内存中执行
编译的各个步骤:
1. 字符流 + 词法分析器                > 符号流
2. 符号流 + 语法分析                  > 语法树
3. 语法树 + 语义分析                  > 语法树
4. 语法树 + 中间代码生成器 + 符号表    > 中间表示形式
5. 中间表示形式 + 机器无关代码优化器   > 中间表示形式(可选)
6. 中间表示形式 + 代码生成器           > 目标机器语言
7. 目标机器语言 + 机器相关代码优化器   > 目标机器语言(可选)

词法分析/Scan

关键字(if, else, while...)
标识符(变量名、数组名...)
常量(const )
运算符(算数 + - * / 关系运算符 == >= !=  逻辑运算符 && || ~)
界限符(; {} () (){};)
position = initial + 60 * rate;
经过词法分析:
<id, 1>       # 词法单元 token
< = >
<id, 2>
< + >
< 60 >
< * >
<id, 3>

语法分析:生成语法分析树

语义分析:

语义检查:(包含*类型检查*)
1. 变量或过程未经声明就使用
2. 变量或过程名重复声明
3. 运算分量类型不匹配(整性和浮点型计算,编译器会把整性转换为浮点型)
4. 操作符与操作数之间类型不匹配,如下
- 数组下标不是整数
- 对非数组变量使用数组访问操作符
- 对非过程名使用过程调用操作符
- 过程调用的参数类型或数目不匹配(参数列表)
- 函数返回类型有误

中间代码生成:

生成一个明确的低级的或类机器语言的表示形式(抽象的机器语言形式)。
可用三地址指令/四元式表示:

x = y op z           (op    ,y,z,x)
x = op y             (op    ,y,_,x)
x = y                (=     ,y,_,x)
if x relop y goto n  (relop,x,y,n)
goto n               (goto  ,_,_,x)
param x              (param ,_,_,n)
call p, n            (call  ,p,n,_)
return x             (return,_,_,x)
x = y[i]             (=[]   ,y,i,x)
x[i] = y             ([]=   ,y,x,i)
x = &y               (&     ,y,_,x)
x = *y               (=*    ,y,_,x)
*x = y               (*=    ,y,_,x)

三地址指令:1. 右侧最多只有一个运算符 
           2. 指令确认了运算完成的顺序
           3. 以临时名字存放三地址指令计算得到的值

代码优化:

改进中间代码,以便生成更好的目标代码。通常意味着更快/更短/耗能更低。

优化前:
t1  = inttofloat(60)
t2  = id3 * t1
t3  = id2 + t2
id1 = t3

优化后:
t1  = id3 * 60.0        // 用 60.0 替代了 inttofloat(60)
id1 = id2 + t1          // 省略了 t3 临时名字

代码生成:

将中间表示形式映射为目标语言。
如果是机器代码,则需要为每个变量选择寄存器和内存地址。

使用寄存器R1,R2: 
<指令> <目标地址>, <...>
LDF  R2, id3             // F 浮点数 LD 加载
MULF R2, R2, #60.0       // 浮点常数 60.0  # 立即数
LDF  R1, id2             // F 浮点数 LD 加载
ADDF R1, R1, R2          // 相加
STF  id1, R1             // STF 存放浮点数

编译技术应用:

针对计算机体系结构的优化:
1. 并行性
 - 指令级并行:硬件调度器动态检测指令流之间的依赖关系,并行发出指令。
 - 指令集:高性能通用微处理器,可以同时对一个向量中的所有数据进行运算。
 - 多处理器:多线程。
相应的编译器技术,可以从顺序的程序出发,将其翻译为指令并行/多处理器代码。


2. 内存层次结构
 - 几层具有不同速度和大小的储存器,离处理器最近的层速度最快,但是容量最小。
 - 对于一个处理器:
 - 寄存器(几百个字节)
 - 高速缓存(KB ~ MB)
 - 物理寄存器(MB ~ GB)
 - 外部存储器(> GB)
高效使用寄存器是优化一个程序要处理的重要的问题。


计算机体系结构设计:
1. RISC 精简指令集计算机
2. CISC 复杂指令集计算机(例如:x86体系结构)
3. 专用体系结构:数据流机器、向量机、VLIW(非常长指令字)机器、SIMD(单指令多数据)处理器阵列、心动阵列、共享内存的多处理器、分布式内存的多处理器。


二进制翻译:一个机器的二进制代码翻译为另一个机器的二进制代码(不同的指令集)。

硬件合成:寄存器传输层(RTL)的硬件设计,在RTL层中,变量代表寄存器,表达式代表组合逻辑。
通过硬件合成工具:RTL描述 -> 门电路 -> 晶体管 -> 物理布局

数据流分析静态地定位错误(静态检测):
1. 类型检查:
 - 类型错误、泛型不匹配
 - 指针为NULL错误释放
 - 字符串是否满足特定格式
2. 边界检查:
 - 例如:缓冲区溢出(C语言没有数组边界检查,必须保证访问不能越界)
 - 复杂的分析技术:跟踪指针值
3. 内存管理工具
 - 垃圾收集机制:效率、易于编程、软件可靠性之间的折衷处理。
 - 自动内存管理:消除内存管理错误,例如内存泄漏(C和C++)
 - Purify:能够动态捕捉内存管理错误的工具,被广泛使用。

程序设计语言基础:

静态(策略):支持编译器静态决定某个问题,这个问题可以在编译时刻决定。
动态(策略):只允许在运行程序的时候做出决定,需要在运行时刻做出决定。

静态作用域:仅通过阅读程序就可以确定一个声明的作用域,或者称为"词法作用域";
动态作用域:当程序运行时,同一个对 x 的使用会指向 x 的几个声明中的某一个。

public static int x;    // 类变量 x

static:  不管创建了多少个类的对象,只存在一个 x 的拷贝,编译器可以确定内存中被用于存放 x 的位置。
无static: 类的每个对象都会有自己用于存放 x 的位置,编译器无法在运行程序之前预先确定所有这些位置。

环境与状态:
1. 环境(environment): 一个从名字到储存位置的映射。或者说:从名字到变量的映射。储存位置/变量,即:"左值"。
2. 状态(state): 一个从内存位置到它们的值的映射。即:状态把"左值"映射为它们的"右值"。

1. 名字到位置的绑定:静态(全局变量,静态变量),动态(局部变量/栈空间变量);
2. 位置到值的绑定:静态(常量,或者宏定义#define),动态(一般是动态的,因为程序运行时才可知道一个位置上的值);

过程、函数和方法:
1. 过程:一个可以被调用的子程序,统称为"过程"。
2. 函数:具有返回值,C语言只有函数,返回类型void不返回任何值,可以看作一个过程。
3. 方法:面向对象,方法总是和某个特定的类关联。Java只有方法,Java一切皆类。

显示控制访问:
1. private私有:限定作用域,这个作用域仅仅包含了该类和友类。
2. protected被保护的:可由子类访问。
3. public公共的:可以从类外访问。

多态过程:
所谓多态过程是指对于同一个名字根据参数类型具有两个或多个定义的过程。

参数传递机制:

  1. 值调用:会对实际参数求值或者拷贝,这些值放在被调用过程相应的形式参数的内存位置上。如果传递的是指针,那么通过指针的拷贝,也可以修改指针指向内存地址的值。传递数组名相当于传递指针。
  2. 引用调用:实际参数的地址作为形式参数的值传递给被调用者,那么使用形式参数时,可以沿着这个指针地址找到实际参数的值并修改。当形式参数是一个大型的对象、数组、结构时,引用调用几乎时必不可少的,因为使用值调用拷贝大量的内存空间到形式参数的空间上,代价高昂。Java对所有不是基本类型的参数都使用了引用调用。
  3. 名调用:实际参数以字面方式替换形式参数,如今已不再采用这种调用机制。

语法制导翻译器

这是一种面向文法的编译技术。

上下文无关文法(context-free grammar),简称"文法";文法被用于组织编译器前端。文法自然地描述了大多数程序设计语言构造的层次化语法结构:由四个元素组成:

  1. 一个终结符号集合。也称"词法单元",基本符号的集合。
  2. 一个非终结符号集合。也称"语法变量",每个非终结符号表示一个终结符号串的集合。
  3. 一个产生式集合。包括产生式头部或左部的非终结符号,一个箭头->,一个产生式体或右部由终结符号及非终结符号组成的序列。
  4. 指定一个非终结符号为开始符号。
if (expression) statement else statement

构造规则:( 箭头->表示:可以具有如下形式)
stmt -> if ( expr ) stmt else stmt

这样的规则成为产生式(production)。像关键字 if 和括号这样的词法元素称为 "终结符号(terminal)",像 expr 和 stmt 这样的变量表示终结符号的序列,称为非终结符号(nonterminal)。

文法定义:

表达式:9 - 5 + 2、 3 - 1、 7
两个数位之间必须出现 + 或 -,这样的表达式成为"由 +、- 号分隔的数位序列"。此文法的产生式包括:
list -> list + digit
list -> list - digit
list -> digit
digit -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

以非终结符号list为头部的三个产生式可以等价地组合为:
list -> list + digit | list - digit | digit
该文法的终结符号包括:+ - 0 1 2 3 4 5 6 7 8 9
该文法的非终结符号是:list、digit,list是此文法的开始符号
一个终结符号串是由零个或多个终结符号组成的序列,零个的串成为空串(empty string),记为epsilon(ε)。

推导:

从开始符号推导得到的所有终结符号串的集合称为该文法定义的语言(language)。

推导 9 - 5 + 2 是一个 list:
1. 因为 9 是digit,根据 list -> digit 可知,9 是list。
2. 因为 5 是digit,且 9 是list,由 list -> list - digit 可知, 9-5 是list。
3. 因为 2 是digit,且 9-5 是list,由 list -> list + digit 可知,9-5+2 也是list。

语法分析(parsing)的任务是:接受一个终结符号串作为输入,找出从文法的开始符号推导出这个串的方法。如果不能从文法的开始符号推导得到该终结符号串,则报告该终结符号串中包含的语法错误。

运算符的结合性:

左结合:当一个运算分量两侧都有左结合符号,它属于左边的运算符。
左结合符号:+ - * /

右结合:当一个运算分量两侧都有右结合符号,它属于右边的运算符。
右结合符号:= += -=
例如:a = b = c 等价于 a = (b = c)

运算符的优先级:

如果 * 先于 + 获得运算分量,我们就说 * 比 + 具有更高的优先级。
例如:9 + 5 * 2 等价于 9 + (5 * 2)

算术表达式的文法:

expr -> expr + term | expr - term | term
term -> term * factor | term / factor | factor
factor -> digit | (expr)


其中:expr A优先级非终结符号 term B优先级非终结符号
其中:factor 表达式中的基本单元 非终结符号
其中:() 带括号的表达式    digit 数位

使用这个文法时,一个表达式就是一个由 + 或 - 分隔开的项(term)的列表,而项(term)是由 * 或 / 分隔的因子(factor)的列表。注意,任何由括号括起来的表达式都是一个因子,因此,我们可以使用括号来构造出具有任意嵌套深度的表达式(以及具有任意深度的语法分析树)。

Java语句的子集的文法:epsilon 表示空

stmt -> id = expression;
      | if (expression) stmt
      | if (expression) stmt else stmt
      | while(expression) stmt
      | do stmt while (expression);
      | { stmts }

stmts -> stmts stmt | epsilon

后缀表示:

  1. 如果 E 是一个变量或常量,则 E 的后缀表示是 E 本身;
  2. 如果 E 是一个形如 E1 op E2 的表达式,其中 op 是一个二目运算符,那么 E 的后缀表示是 E'1 E'2 op,这里 E'1 和 E'2 分别是 E1 和 E2 的后缀表示;
  3. 如果 E 是一个形如 (E1) 的被括号括起来的表达式,则 E 的后缀表示就是 E1 的后缀表示。
例如:
中缀表示:(9 - 5) + 2
后缀表示:9 5 - 2 +

中缀表示:9 - (5 + 2)
后缀表示:9 5 2 + -

运算符的位置和它的运算分量个数使得后缀表达式只有一种解码方式,所以在后缀表示中不需要括号。处理后缀表达式的技巧就是从左边开始不断扫描后缀串,知道发现一个运算符为止。然后向左找出适当数目的运算分量,并将这个运算符和它的运算分量组合在一起。计算出这个运算符作用于这些运算分量是得到的结果,并用这个结果替换原来的运算分量和运算符。然后继续这个过程,向右搜寻另一个运算符。

从左开始扫描:9 5 2 + - 3 *
遇到 + 号:5 2 + 替换为 7
继续扫描:9 7 - 3 *
遇到 - 号:9 7 - 替换为 2
继续扫描:2 3 *
遇到 * 号:2 3 * 替换为 6
计算结果:6
posted @ 2025-12-16 00:26  北纬31是条纬线哦  阅读(9)  评论(0)    收藏  举报