Chapter 5 — step_5 RISC-V 代码生成与 QEMU 运行

对应实践Lab05 - step_5 代码生成 主要修改文件miniCompiler_lab/labs/lab05-step5/framework/student.c

前面的章节一直在做“理解程序”这件事:读文件、切 token、建 AST、做语义检查。到了这一章,编译器终于要把这种理解变成对目标机器真正有用的东西:RISC-V 汇编。

这一步和前面最大的不一样在于:从这里开始,你写错一行逻辑,错误不一定只体现在“树形不对”或“报错信息不对”,而可能直接变成程序运行结果错误、返回值不对、甚至汇编根本汇不起来。

5.1 为什么代码生成不是“把 AST 翻译成文本”这么简单

表面上看,代码生成器做的事情好像只是 fprintf 汇编字符串。
但它真正同时要处理的事情有四类:

  1. 表达式结果该落在哪个寄存器里
  2. 临时结果需要怎样压栈和弹栈
  3. 局部变量在当前栈帧中的偏移是多少
  4. ifwhilereturn 这些控制流要怎样变成跳转和标签

也就是说,代码生成器不是“会打印文本”就够了,而是必须带着一个很明确的目标机器执行模型去打印文本。

5.2 这一章最重要的直觉:栈帧不是附属细节

在代码生成这一章里,你会不断遇到这些名字:

  • sp
  • s0
  • ra
  • 栈偏移
  • prologue / epilogue

这些东西不是汇编里的仪式感摆设,而是局部变量、函数调用和返回过程真正赖以成立的基础设施。

如果你不知道变量 sum 在栈帧里离 s0 偏移多少,你就没法把 Ident: sum 变成正确的 lw 指令。
如果你不知道返回地址该如何保存和恢复,函数调用一多,程序流程就会直接乱掉。

所以这一章要建立的不是“几条汇编模板”,而是“AST 节点怎样落到具体机器状态”。

5.3 先看一个最真实的闭环

从这一章开始,验证不再只是“结构对不对”,还要看生成的汇编能不能被工具链接受。你会反复遇到这个闭环:

source.c -> token -> AST -> semantic check -> RISC-V asm -> qemu

这里有两件非常关键的事情:

  1. 编译器真的生成了一份 RISC-V 汇编
  2. 这份汇编真的被汇编、链接并交给 qemu-riscv32 跑了起来

前面所有章节,都是在为这里铺路。

5.4 表达式代码生成为什么总绕不开栈

当你生成 x + y * z 这样的表达式时,不可能把所有中间结果都同时留在一个寄存器里。所以常见做法是:

  1. 先算右边
  2. 暂时压栈
  3. 再算左边
  4. 把右边弹出来放到另一个寄存器
  5. 执行具体运算

你在 lab 里接触到的 addi sp, sp, -4swlwaddi sp, sp, 4,本质上都在做这件事。

这类代码第一次看会觉得繁琐,但只要你抓住“一个子表达式结果要暂存,栈就是最直接的临时仓库”这个直觉,很多看似机械的指令序列就会变得顺眼。

5.5 if 和 while 为什么离不开标签

高级语言里的 ifwhile 写起来像结构,落到汇编里却只能变成跳转。

这就是为什么代码生成器要维护 label_count,不断生成 .L1.L2 这种局部标签。
在 AST 里你看到的是:

  • 条件
  • then 分支
  • else 分支

在汇编里你最终要安排的是:

  • 条件为假时跳去哪里
  • then 分支执行完后跳去哪里
  • 循环体结束后回到哪里

也就是说,控制流代码生成本质上是在把树结构重新压平为带标签的跳转图。

5.6 本章 practice 边界

这一章的 practice 路径是:

  • miniCompiler_lab/labs/lab05-step5/

主要修改文件:

  • framework/student.c

验证命令:

cd labs/lab05-step5
make clean && make test

这一章的 lab 会把代码生成器收缩成三块最关键的骨架:

  1. student_get_var_offset
  2. student_gen_expr
  3. student_gen_stmt

这三块刚好覆盖了后端最关键的三个视角:

  • 变量在栈里的位置怎么找到
  • 表达式怎么落成寄存器与指令
  • 语句和控制流怎么落成跳转和函数结构

5.7 这一章真正的验证,不是“打印对了”,而是“跑起来了”

这一章一定要把验证目标盯准。
如果你只是看输出文件里“好像有几条 addlwsw”,那还不够。

真正值得相信的验证是:

  1. 汇编可以通过 asld
  2. qemu-riscv32 能运行它
  3. 程序退出码和预期一致

因为只有到这一步,编译器生成的代码才算真的接触到了目标机器语义。

5.8 本章小结

这一章把整门课真正收口了。前面的前端和语义阶段负责把程序理解清楚,这一章负责把这种理解变成目标机器能够执行的汇编。

到这里,minicc 已经不再只是一个“会打印 token 和 AST 的教学程序”,而是一个可以把 C 子集编译到 RISC-V 并实际运行的迷你编译器。

5.9 下一步

现在去完成:

  1. Lab05 - step_5 代码生成
  2. 让生成的汇编在 qemu-riscv32 中真正跑起来
  3. 然后回看附录,补齐你还想深入的部分