step10 实验指导
本实验指导使用的例子为:
int x = 2024;
int main() { return x; }
词法语法分析
针对全局变量,我们需要新设计 AST 节点来表示它,只需修改根节点的孩子类型即可:原先表示整个 MiniDecaf 程序的根节点只能有函数类型的子节点,现在还可以允许变量声明作为子节点。
语义分析
本步骤引入全局变量,在引入全局变量之后,AST 根结点的直接子结点不只包括函数,还包括全局变量定义。全局变量符号存放在栈底的全局作用域符号表中。在遍历 AST 构建符号表的过程中,栈底的全局作用域符号表一直都存在,不会被弹出。
中间代码生成
经过 Step5 的学习,我们知道局部变量是存储在寄存器或栈中的,可以直接访问。然而,全局变量存储在特别的内存段中,不能直接访问。课程实验建议的加载全局变量方式为:首先加载全局变量符号的地址,然后根据地址来加载数据。因此,需要定义两个中间代码指令,完成全局变量值的加载:
请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。
指令 | 参数 | 含义 |
---|---|---|
LOAD |
T1, offset |
临时变量 T1 中存储地址,加载与该地址相差 offset 个偏移的内存地址中的数据 |
LOAD_SYMBOL |
symbol |
symbol 为字符串,加载 symbol 符号所代表的地址 |
有了上述两条指令,可以将测试用例翻译如下:
main:
_T0 = LOAD_SYMBOL x
_T1 = LOAD _T0, 0
return T1
需要说明的是,你也可以把两条指令合并为一条指令,直接加载全局变量的值,但分为两条指令的方式可扩展性更好些。
请注意,翻译所得的 TAC 代码中没有为全局变量赋予初始值(2024)。可以将变量的初始值存放在变量符号对应的符号表里,在后端代码生成时通过读取符号表得到初值。此处给出的只是一种参考实现,大家也可以将全局变量的定义显式翻译为 TAC 代码,这样可以使中端与后端完全解耦。
目标代码生成
Step10 中目标代码生成的主要任务有:翻译中间代码,将全局变量放到特定的数据段中。
翻译中间代码
实际上,我们提供的中间代码设计和 RISC-V 汇编的思想是一致的,RISC-V 汇编中有对应 LOAD 和 LOAD_SYMBOL 的指令,我们直接给出翻译结果:
main: la t0, x # _T0 = LOAD_SYMBOL x lw t1, 0(t0) # _T1 = LOAD _T0, 0 mv a0, t1 ret
将全局变量放到特定的数据段中
到目前为止,翻译中间代码的方式是有问题的,问题在于,需要加载的 x 变量符号究竟存在哪里,如果所生成的汇编程序不给出 x 的定义,程序是有bug的。实际上,RISC-V 提供了一系列的汇编指令,用以声明全局变量 x 所对应的数据段。
下面给出 RISC-V 用以全局变量声明的汇编指令,其他全局变量的声明只需修改变量名称和初始值即可:
.data .globl x x: .word 2024
上例中,.data 表示输出到 data 数据段;.globl x 声明 x 为全局符号;.word 后是一个 4 字节整数,是 x 符号对应的初始值。
按照汇编约定,data 段中存放已初始化的全局变量,未初始化的全局变量则存放在 bss 段中。举例而言,下面的示例将未初始化的全局变量 x 存放到 bss 段中。其中,.space 表示预留一块连续的内存,4 表示存储空间大小为 4 字节。
.bss .globl x x: .space 4
思考题
- 写出
la v0, a
这一 RiscV 伪指令可能会被转换成哪些 RiscV 指令的组合(说出两种可能即可)。
参考的 RiscV 指令链接:https://github.com/TheThirdOne/rars/wiki/Supported-Instructions