课程主页:

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。