9.4 KiB
Lab3 实验记录:指令选择与汇编生成
1. 实验目标
本次 Lab3 的目标是在已有的 SysY 前端与 IR 生成基础上,补齐 AArch64 后端指令选择、控制流翻译、全局变量和运行时库接口,使编译器能够把 SysY IR 翻译为可在 AArch64(ARM64)平台上运行的汇编程序,并通过 QEMU 模拟器验证生成结果的正确性。
本次完成工作的重点包括:
- 扩展 MIR 中物理寄存器、指令操作数种类与机器指令集,完整覆盖 AArch64 核心子集。
- 扩展指令选择逻辑(
Lowering.cpp),支持多函数、多基本块、函数调用、浮点数与多维数组(GEP)地址计算。 - 处理 AArch64 调用约定(ABI)中参数传递(整数/浮点前 8 传参)与栈帧落地细节。
- 解决 AArch64 特有的指令寻址与栈槽大偏移(超出 ldur/stur 范围)的物理寄存器备用搬运机制。
- 补齐 SysY 运行时库(
sylib/sylib.c)中所有 I/O、时间统计与十六进制浮点输入输出功能。
2. 代码改动范围
本次实验主要修改/新增了以下文件:
include/mir/MIR.h与src/mir/MIRFunction.cpp、src/mir/MIRInstr.cpp、src/mir/Register.cpp、src/mir/RegAlloc.cpp、src/mir/FrameLowering.cppsrc/mir/Lowering.cpp(核心指令选择)src/mir/AsmPrinter.cpp(核心汇编文本打印)sylib/sylib.c(SysY 运行库)scripts/verify_asm.sh(自动化编译链接脚本)src/main.cpp(后端多函数汇编流适配)src/irgen/IRGenExp.cpp(修复前端常数类型转换缺陷)- 新增本文档
doc/Lab3-实验记录.md
3. 完成过程
3.1 梳理后端结构与定位边界
阅读了实验文档 doc/Lab3-指令选择与汇编生成.md,原有的后端属于“极简演示”:
- 仅支持单函数
main与单基本块。 - 仅支持
alloca,load,store,add,ret五种指令。 - 栈帧偏移与寻址硬编码为
ldur/stur,没有考虑多维数组、浮点数以及超出[-256, 255]寻址范围的指令级溢出崩溃问题。
3.2 解决前置类型转换 bug
在回归测试 95_float.sy 时,我们发现由于前端对 const int 类型常量初始值为 float 时没有及时阶段性类型截断,导致 const int FIVE = TWO + THREE(其中 TWO = 2.9, THREE = 3.2)的编译期常量求值被错误地计算为 2.9 + 3.2 = 6.1 再向下转型为 6,而实际应该先将 TWO 转型为 2,THREE 转型为 3,二者相加得到 5。
我们在 IRGenExp.cpp 的 ConstExprVisitor::visitLValueExp 中实现了类型安全截断,彻底解决了这一隐式类型转换带来的精度和常量值错误。
3.3 AArch64 后端指令扩充与栈槽模型构建
我们保持并完善了后端的高可靠“栈槽模型”:
- 每一个 IR 中产生的
Value(包括临时虚拟寄存器和指令)均在LowerToMIR中分配一个专属的 64 位(或 32 位)栈槽(FrameIndex)。 - 在 lowering 每一条指令时,先从它们的栈槽加载操作数到 AArch64 的 scratch 寄存器(
w8/w9或s8/s9等),执行运算后再把结果写回栈槽。 - 这种模型虽然带来了一定的访存冗余(可通过 Lab5 寄存器分配和窥孔优化消除),但在本阶段能够 100% 保证变量活跃期与正确性,排除了寄存器冲突。
4. 关键困难与解决办法
4.1 困难一:双向迭代器/指针失效(BasicBlock vector 重配引发的段错误)
现象
在对包含复杂控制流的用例(如 29_break.sy)进行编译时,后端经常发生 段错误(Segmentation Fault)。
经过定位,我们在 LowerToMIR 发现,基本块是通过 machine_func->CreateBlock(bbPtr->GetName()) 动态添加进 std::vector<MachineBasicBlock> blocks_ 中的。随着 blocks vector 容量扩张,底层的内存发生重分配,导致此前在 std::unordered_map<const ir::BasicBlock*, MachineBasicBlock*> bb_map 中记录的所有指向 MachineBasicBlock 的指针全部变成了野指针(Dangling Pointer),再次使用时引发段错误。
解决办法
在创建基本块循环前,预先调用 machine_func->GetBlocks().reserve(func.GetBlocks().size()) 保障 vector 拥有足够容量,彻底杜绝了动态重分配带来的指针失效问题。
4.2 困难二:栈帧槽寻址大偏移超出 AArch64 立即数范围
现象
在 25_scope3.sy 和 95_float.sy 中,函数内临时变量繁多,栈帧空间轻松超过 256 字节。AArch64 的 ldur/stur 的非对齐 9 位带符号偏移限制在 [-256, 255] 范围内。一旦栈帧偏移动态计算结果为 -268 等越界值,汇编器(as)便会报错 immediate offset out of range 拒绝编译。
解决办法
在 AsmPrinter.cpp 的 PrintStackAccess 寻址生成中增加偏移区间自适应检测:
- 若偏移量在
[-256, 255]之间,照常生成轻量的ldur/stur; - 若偏移量超出该区间,则先生成
mov x10, #offset汇编指令将偏移加载至备用 64 位寄存器x10,然后再使用 AArch64 的寄存器偏移寻址格式ldr reg, [x29, x10]或str reg, [x29, x10]完美避开立即数范围限制。
4.3 困难三:浮点常量与全局变量打印的精度丢失
现象
95_float.sy 中对浮点数相等的比较非常苛刻。如果全局浮点变量打印为 .float 3.14159,在 C++ ostream 默认 6 位精度输出下会造成严重的低位比特丢失,导致十六进制浮点输入输出断言失败。
解决办法
我们将所有全局和局部的浮点常数转换为底层的 bit-exact 二进制字面量表示。例如浮点数 val,先通过 memcpy 获取其 32 位整型二进制比特,然后以 .word <bits> 指令原封不动写回汇编。这保证了在编译、汇编、运行的全生命周期中,浮点数值是 100% 位一致 的。
4.4 困难四:SysY 库函数接口的缺失与十六进制浮点适配
现象
由于原仓库的 sylib/sylib.c 是一个空壳,导致调用了 I/O 运行库的测试用例链接失败。并且评测指标中浮点数的输入输出要求使用十六进制浮点格式(%a)输出。
解决办法
- 完整用 C 语言重写了
sylib/sylib.c,提供getint,getch,getfloat,getarray,getfarray,putint,putch,putfloat,putarray,putfarray,starttime,stoptime的高可靠实现。 - 将
putfloat和putfarray适配为%a十六进制浮点格式,同时采用double精度读取以消除单双精度转换过程中的尾数舍入偏差。 - 修改
verify_asm.sh,在汇编可执行文件生成阶段自动打包链接sylib/sylib.c。
5. 本次实现的主要能力
本阶段完成后,后端编译器已具备以下完整功能:
- AArch64 指令覆盖:支持算术(
add,sub,mul,sdiv,msub)、比较(cmp,fcmp)、条件选择(cset)、控制流分支(b,b.cond)、函数调用(bl)、内存传输(ldr,str,ldur,stur)、浮点数转换(scvtf,fcvtzs)。 - ABI 调用约定规范:完整实现了前 8 个整型/指针参数及前 8 个浮点参数通过寄存器传递,返回结果分别放入
w0/x0/s0。 - 多函数多块控制流:支持具有任意多非声明函数、多基本块的控制流图(CFG)后端降低。
- 高保真浮点系统:支持 bit-perfect 浮点常数生成和位级别精确度全局变量初始化。
- 大栈帧保障寻址:突破 AArch64 立即数偏移寻址范围,保障任意超大型函数的安全编译。
6. 验证结果
我们对 test/test_case/functional 目录下的所有用例执行了汇编与执行回归。所有用例均成功生成 AArch64 汇编,成功链接运行库,且运行输出结果与退出码与预期文件(.out)100% 吻合,完全通过:
=== Running test/test_case/functional/05_arr_defn4.sy ===
输出匹配: test/test_case/functional/05_arr_defn4.out
=== Running test/test_case/functional/09_func_defn.sy ===
输出匹配: test/test_case/functional/09_func_defn.out
=== Running test/test_case/functional/11_add2.sy ===
输出匹配: test/test_case/functional/11_add2.out
=== Running test/test_case/functional/13_sub2.sy ===
输出匹配: test/test_case/functional/13_sub2.out
=== Running test/test_case/functional/15_graph_coloring.sy ===
输出匹配: test/test_case/functional/15_graph_coloring.out
=== Running test/test_case/functional/22_matrix_multiply.sy ===
输出匹配: test/test_case/functional/22_matrix_multiply.out
=== Running test/test_case/functional/25_scope3.sy ===
输出匹配: test/test_case/functional/25_scope3.out
=== Running test/test_case/functional/29_break.sy ===
输出匹配: test/test_case/functional/29_break.out
=== Running test/test_case/functional/36_op_priority2.sy ===
输出匹配: test/test_case/functional/36_op_priority2.out
=== Running test/test_case/functional/95_float.sy ===
输出匹配: test/test_case/functional/95_float.out
=== Running test/test_case/functional/simple_add.sy ===
输出匹配: test/test_case/functional/simple_add.out
7. 结论
本次 Lab3 完成了后端指令选择与汇编生成的完美跨越,成功将一个“玩具”后端重构成了一个支持多函数、多基本块、复杂数组与完整浮点运算的高可靠 AArch64 生成引擎。阻塞链路的所有底层越界与精度问题已被完美解决,为 Lab4-6 的标量优化、寄存器分配以及循环分析打下了极其坚实的后端基石。