Stanford Compiler Week 6 Code Generation Part 1
课程主页:
https://www.edx.org/course/compilers
课程视频:
https://www.bilibili.com/video/BV1NE411376V?p=20&t=6
备注:
图片均来自于课件。
这次回顾代码生成第一部分。
代码生成简介
- 我们专注于为带有累加器的堆栈机生成代码;
 - 我们想在真实的机器上运行生成的代码,例如MIPS处理器(或模拟器);
 - 我们使用MIPS指令和寄存器模拟堆栈机指令;
 - 累加器保存在MIPS寄存器$\mathrm{$ a_0}$中;
 - 堆栈保存在内存中
- 堆栈向低地址生长,这是MIPS的标准约定。
 
 - 堆栈中下一个位置的地址保存在MIPS寄存器$\mathrm{$sp}$中
- 栈顶在地址$\mathrm{$sp} +4$处。
 
 
MIPS简介
MIPS架构是
- 原型精简指令集计算机(RISC)
 - 大多数操作都将寄存器用于操作数和结果
 - 使用load和store指令在内存中使用值
 - 32个通用寄存器(每个32位)
- 我们使用$\mathrm{$ sp,$a0}$和$\mathrm{$t1}$(临时寄存器)
 
 - 阅读SPIM模拟器文档以获取详细信息。
 
常用指令
- $\text {lw reg}_{1} \text { offset}(\mathrm{reg}_{2})$
- 从$\mathrm{reg}_{2} + \text{offset}$加载32比特字到$\text{reg}_1$
 
 - $\text {add } \mathrm{reg}_{1} \text { reg}_{2} \text { reg}_{3}$
- $\mathrm{reg}_{1} \leftarrow \mathrm{reg}_{2}+\mathrm{reg}_{3}$
 
 - $\text {sw reg}_{1} \text { offset}(\text {reg}_{2})$
- 从$\text{reg}_1$加载32比特字到$\mathrm{reg}_{2} + \text{offset}$
 
 - ${\text {addiu reg}_{1} \text { reg}_{2} \text { imm}}$
- $\mathrm{reg}_{1} \leftarrow \mathrm{reg}_{2}+\mathrm{imm}$
 - “$\text{u}$”意味着不查看溢出
 
 - $\text {li reg imm }$
- $\text {reg} \leftarrow \text {imm }$
 
 
例子
考虑MIPS中$7 + 5$的堆栈机代码:
| 堆栈机代码 | MIPS汇编代码 | 
|---|---|
| $\mathrm{a c c} \leftarrow 7$ | $\mathrm {li\ $a0\ 7}$ | 
| $\text {push acc}$ | $\mathrm{sw\ $a 0\ 0($s p)}$ | 
| $\mathrm {addiu\ $sp\ $sp-4}$ | |
| $\mathrm{a c c} \leftarrow 5$ | $\mathrm {li\ $a0\ 5}$ | 
| $\text {acc} \leftarrow \text{acc + top_of_stack }$ | $\mathrm {lw\ $t1\ 4($sp) }$ | 
| $\mathrm {add\ $ a0\ $a0\ $t1}$ | |
| $\text{pop}$ | $\mathrm {addiu \ $ sp\ $sp+4}$ | 
代码生成1
讨论的语言
考虑具有整数和整数运算的语言
对于该语言
第一个函数定义$\mathrm f$是入口
- “main”程序
 
我们可以写出斐波纳契数的程序:
该语言的代码生成
- 对于每个表达式$\mathrm e$,我们生成以下MIPS代码:
- 计算$\mathrm {$a0}$中$\mathrm e$的值
 - 保留$\mathrm {$sp}$和堆栈中的内容
 
 - 我们定义一个代码生成函数$\mathrm{cgen(e)}$,其结果是为$\mathrm e$生成的代码。
 
常数代码生成
评估常数的代码只是将其复制到累加器中:

其中
- 红色:编译时
 - 蓝色:运行时
 
加法的代码生成
考虑加法的例子:

代码说明:
- $\mathrm{sw\ $ a0\ 0($ sp)}$将$\mathrm{$a0}$存储到栈顶的位置。
 - $\mathrm {addiu\ $sp\ $ s p\ -4}$更新栈顶位置。
 - $\mathrm{lw\ $t1\ 4($sp)}$将$\mathrm{$a0}$存储到$\mathrm{$t1}$。
 - $\mathrm{add\ $a0\ $t1\ $a0}$对应$\mathrm{e_1 +e_2}$
 - $\mathrm {addiu\ $sp\ $ s p\ +4}$更新栈顶位置。
 
注意不能将$\mathrm e_1$的结果直接保存在$\mathrm {$t1}$中:

考虑如下反例
1 + (2 + 3)
对应的mips代码
li		$a0		1
move 	$t1 	$a0
li		$a0 	2
move	$t1 	$a0
li		$a0 	3
add		$a0 	$t1 	$a0
add		$a0 	$t1 	$a0
#$a0 = 2 + 3 + 2 = 7
讨论
$\mathrm{+}$的代码是一个模板,该模板包括$\mathrm{e_1}$和$\mathrm{e_2}$部分加上”hole”:
代码:
code(e1) ... code(e2) ...
堆栈机代码生成是递归的:
- $\mathrm{e_1 + e_2}$的代码是将$\mathrm{e_1}$和$\mathrm{e_2}$代码粘合在一起。
 
代码生成可以写为AST的递归下降
- 至少对于表达式。
 
减法的代码生成
新指令:$\text {sub } \mathrm{reg}_{1} \text { reg}_{2} \text { reg}_{3}$
- 实现功能:$\mathrm{reg}_{1} \leftarrow \mathrm{reg}_{2}-\mathrm{reg}_{3}$
 

条件指令
新指令:$\text {beq reg}_{1} \text { reg}_{2} \text { label }$
- 实现功能:如果$\mathrm{reg_1 = reg_2}$,则跳转到$\text{label}$。
 
新指令:$\text{b label}$
- 无条件跳转到$\text{label}$
 
MIPS代码

习题

分析:
如下代码对应$(4 - 3)$
li $a0 4
sw $a0 0($sp)
addiu $sp $sp -4
li $a0 3
lw $t1 4($sp)
sub $a0 $t1 $a0
所以选择第一项。
代码生成2
函数调用和函数定义的代码取决于AR的布局
对于此此语言,非常简单的AR就足够了:
- 结果始终在累加器中
- 无需将结果存储在AR中
 
 - 激活记录保存实际参数
- 对于$\mathrm {f(x_1,\ldots,x_n)}$,将$\mathrm {x_n,\ldots, x_1}$推入堆栈
 - 这些是该语言中唯一的变量
 
 
- 结果始终在累加器中
 堆栈规则保证$\mathrm{$sp}$在函数退出与函数开始前
- 无需控制链接
 
我们需要返回地址
指向当前激活的指针很有用
- 该指针位于寄存器$\mathrm{$fp}$(栈帧指针)中
 
总结:对于这种语言,带有caller栈帧指针,实际参数和返回地址的AR就足够了
图示:考虑对$\mathrm{f(x,y)}$的调用,AR是:


调用序列是(调用方和被调用方的)设置函数调用的指令。
jal指令
新指令:$\text{jal label}$
- 跳转至label,将下一条指令的地址保存在$\mathrm {$ra}$中
 - 在其他架构上,返回地址通过“调用”指令存储在堆栈中
 
调用者保存其帧指针的值
然后以相反的顺序保存实际参数
最后,调用者将返回地址保存在寄存器$\mathrm {$ra}$中
到目前为止,AR为$4 \times n + 4$字节长
例子:

jr指令
- 新指令:$\text{jr reg}$
- 跳转到寄存器$\text{reg}$中的地址
 
 - 注意:栈帧指针指向栈帧的顶部,而不是底部
 - 被调用者弹出返回地址,实际参数和帧指针的保存值
 - $\mathrm {z = 4 \times n + 8}$
- $8$对应返回地址和旧栈帧
 
 - 例子:
 

变量引用
变量引用是最后的构造
函数的“变量”只是其参数
- 它们都在AR中
 - 被caller push
 
问题:由于在保存中间结果时堆栈会增长,因此变量与$\mathrm{$sp}$的偏移量不是固定的。
解决方案:使用栈帧指针
- 始终指向堆栈上的返回地址
 - 由于它不移动,因此可以用来查找变量
 
令$\mathrm{x_i}$为为其生成代码的函数的第$\mathrm{i}$个($\mathrm{i=1,\ldots,n}$)形式参数
例子:对于函数$\text{def f(x,y) = e}$,激活和帧指针的设置如下:

习题

选择第一和第四项。
小结
- 激活记录必须与代码生成器一起设计;
 - 可以通过递归遍历AST来完成代码生成;
 - 我们建议您为Cool编译器使用堆栈机(因为这很简单);
 - 生产级编译器做不同的事情;
 - 重点在于将值保存在寄存器中
- 特别是当前的栈帧;
 - 中间结果放置在AR中,而不是从堆栈中push和pop。