step10 实验指导

本实验指导使用的例子为:

int x = 2023;
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 代码中没有为全局变量赋予初始值(2023)。可以将变量的初始值存放在变量符号对应的符号表里,在后端代码生成时通过读取符号表得到初值。此处给出的只是一种参考实现,大家也可以将全局变量的定义显式翻译为 TAC 代码,这样可以使中端与后端完全解耦。

目标代码生成

Step10 中目标代码生成的主要任务有:翻译中间代码,将全局变量放到特定的数据段中。

  1. 翻译中间代码

    实际上,我们提供的中间代码设计和 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
    
  2. 将全局变量放到特定的数据段中

    到目前为止,翻译中间代码的方式是有问题的,问题在于,需要加载的 x 变量符号究竟存在哪里,如果所生成的汇编程序不给出 x 的定义,程序是有bug的。实际上,RISC-V 提供了一系列的汇编指令,用以声明全局变量 x 所对应的数据段。

    下面给出 RISC-V 用以全局变量声明的汇编指令,其他全局变量的声明只需修改变量名称和初始值即可:

    .data
    .globl x
    x:
        .word 2023
    

    上例中,.data 表示输出到 data 数据段;.globl x 声明 x 为全局符号;.word 后是一个 4 字节整数,是 x 符号对应的初始值。

    按照汇编约定,data 段中存放已初始化的全局变量,未初始化的全局变量则存放在 bss 段中。举例而言,下面的示例将未初始化的全局变量 x 存放到 bss 段中。其中,.space 表示预留一块连续的内存,4 表示存储空间大小为 4 字节。

    .bss
    .globl x
    x:
        .space 4
    

思考题

  1. 写出 la v0, a 这一 RiscV 伪指令可能会被转换成哪些 RiscV 指令的组合(说出两种可能即可)。

参考的 RiscV 指令链接:https://github.com/TheThirdOne/rars/wiki/Supported-Instructions

results matching ""

    No results matching ""

    results matching ""

      No results matching ""