Skip to content

Commit a5f9b8b

Browse files
committed
[feat]: add docs of task5
1 parent dc055b4 commit a5f9b8b

5 files changed

Lines changed: 527 additions & 1 deletion

File tree

docs/_sidebar.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
- [API 速查](task4_doc/apidoc.md)
3838
- Task5
3939
- [整体介绍](task5_doc/overview.md)
40+
- [RV64 指令集参考](task5_doc/riscv.md)
41+
- [填空导读](task5_doc/guide.md)
42+
- [API 速查](task5_doc/apidoc.md)
4043
- Q&A
4144
- [常见问题与答案](QA.md)
4245
- 致谢

docs/task5_doc/apidoc.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# API 速查
2+
3+
填空时会频繁用到以下函数,这里做一个快速索引。
4+
5+
## 核心辅助函数
6+
7+
| 函数 | 作用 |
8+
| ------------------------------------ | ----------------------------------------- |
9+
| `emitLoadValue(v)` | 把 LLVM Value 变成可用的虚拟寄存器(常量会先 load 进临时寄存器) |
10+
| `vregOf(v)` | 查找 LLVM Value 对应的虚拟寄存器 |
11+
| `nextTempVReg()` | 取一个临时虚拟寄存器,用于中间计算 |
12+
13+
## 指令生成函数
14+
15+
### 算术运算
16+
17+
| 函数 | 对应指令 | 说明 |
18+
| ----------- | ---- | ----------- |
19+
| `emitVAdd` | ADD | 寄存器加法 |
20+
| `emitVSub` | SUB | 寄存器减法 |
21+
| `emitVMul` | MUL | 乘法 |
22+
| `emitVDiv` | DIV | 有符号除法 |
23+
| `emitVRem` | REM | 有符号取余 |
24+
25+
### 位运算
26+
27+
| 函数 | 对应指令 | 说明 |
28+
| ----------- | ---- | -------- |
29+
| `emitVAnd` | AND | 按位与 |
30+
| `emitVOr` | OR | 按位或 |
31+
| `emitVXor` | XOR | 按位异或 |
32+
| `emitVXori` | XORI | 异或立即数 |
33+
34+
### 移位
35+
36+
| 函数 | 对应指令 | 说明 |
37+
| ------------ | ---- | ------------ |
38+
| `emitVSll` | SLL | 逻辑左移(寄存器) |
39+
| `emitVSrl` | SRL | 逻辑右移(寄存器) |
40+
| `emitVSra` | SRA | 算术右移(寄存器) |
41+
| `emitVSlli` | SLLI | 逻辑左移(立即数) |
42+
| `emitVSrli` | SRLI | 逻辑右移(立即数) |
43+
| `emitVSrai` | SRAI | 算术右移(立即数) |
44+
45+
### 比较
46+
47+
| 函数 | 对应指令 | 说明 |
48+
| ------------ | ----- | ----------- |
49+
| `emitVSlt` | SLT | 有符号小于比较 |
50+
| `emitVSltu` | SLTU | 无符号小于比较 |
51+
| `emitVSltiu` | SLTIU | 无符号小于立即数比较 |
52+
53+
### 访存
54+
55+
| 函数 | 对应指令 | 说明 |
56+
| --------------- | ---- | -------- |
57+
| `emitVLoad` | LD | 8 字节读取 |
58+
| `emitVStore` | SD | 8 字节写入 |
59+
| `emitVLoad32` | LW | 4 字节读取 |
60+
| `emitVStore32` | SW | 4 字节写入 |
61+
62+
### 常用工具
63+
64+
| 函数 | 对应指令 | 说明 |
65+
| -------------------------- | ----------------- | ---------- |
66+
| `emitVLoadImm(dst, imm)` | `ADDI dst, x0, imm` | 加载立即数 |
67+
| `emitVMov(dst, src)` | `ADDI dst, src, 0` | 寄存器复制 |
68+
69+
## 类型查询
70+
71+
| 函数 | 作用 |
72+
| --------------------------- | ------------ |
73+
| `dl_.getTypeAllocSize(ty)` | 获取类型占用的字节数 |

docs/task5_doc/guide.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# 填空导读
2+
3+
`EmitMIR.cpp` 中有四处 `TODO: Student Implementation`,下面逐一说明思路。
4+
5+
## 1 emitBinary — 二元运算
6+
7+
需要做的事:把 LLVM IR 的二元运算翻译成对应的 RV64 指令。
8+
9+
**基本步骤**
10+
11+
1.`emitLoadValue` 取出左右操作数,用 `vregOf` 拿到目标寄存器。
12+
2.`bo.getOpcode()` 做 switch 分发。
13+
14+
**各 opcode 的对应关系**
15+
16+
17+
| LLVM opcode | RV64 辅助函数 |
18+
| ----------- | ---------- |
19+
| `Add` | `emitVAdd` |
20+
| `Sub` | `emitVSub` |
21+
| `Mul` | `emitVMul` |
22+
| `SDiv` | `emitVDiv` |
23+
| `SRem` | `emitVRem` |
24+
| `And` | `emitVAnd` |
25+
| `Or` | `emitVOr` |
26+
| `Xor` | `emitVXor` |
27+
28+
29+
**移位需要额外处理**
30+
31+
`Shl``LShr``AShr` 三种移位的右操作数可能是编译期常量。如果是常量,优先用立即数版本(`emitVSlli` / `emitVSrli` / `emitVSrai`),否则用寄存器版本(`emitVSll` / `emitVSrl` / `emitVSra`)。
32+
33+
判断方法:对 `bo.getOperand(1)``dyn_cast<ConstantInt>`,如果成功就拿 `getSExtValue()` 作为立即数。
34+
35+
**别忘了 default 分支**
36+
37+
对不支持的 opcode 调用 `llvm::report_fatal_error`,不要静默忽略。
38+
39+
## 2 emitICmpInst — 整数比较
40+
41+
需要做的事:把 LLVM IR 的 `icmp` 翻译成 0/1 结果。
42+
43+
**基本步骤**
44+
45+
1.`emitLoadValue` 取左右操作数,`vregOf` 拿目标寄存器。
46+
2.`nextTempVReg()` 拿一个临时寄存器。
47+
3.`ci.getPredicate()` 做 switch 分发,对照 [比较指令组合表](riscv.md#从-sltsltu-组合出所有比较谓词重要) 实现。
48+
49+
**关键点**
50+
51+
- `ICMP_EQ`:先 XOR 求差,再 `SLTIU dst, tmp, 1` 判断差是否为 0。
52+
- `ICMP_NE`:先 XOR 求差,再 `SLTU dst, x0, tmp` 判断差是否非 0。这里用到了 `x0` 寄存器(`llvm::RISCV::X0`)。
53+
- `ICMP_SGT`:交换操作数后用 SLT — `SLT dst, rhs, lhs`
54+
- `ICMP_SLE` / `ICMP_SGE`:先做严格比较,再用 `XORI dst, tmp, 1` 取反。
55+
- 无符号版本(`ICMP_ULT` 等)把 SLT 换成 SLTU,逻辑完全对称。
56+
57+
## 3 emitLoadInst — load 指令
58+
59+
需要做的事:从内存地址读取值到寄存器。
60+
61+
**基本步骤**
62+
63+
1.`emitLoadValue` 取出地址操作数(`li.getPointerOperand()`)。
64+
2.`vregOf` 拿目标寄存器。
65+
3.`dl_.getTypeAllocSize(li.getType())` 拿到被读取类型的大小。
66+
4. 如果大小是 4 字节,调用 `emitVLoad32`;否则调用 `emitVLoad`
67+
68+
就这么多,非常直接。
69+
70+
## 4 emitStoreInst — store 指令
71+
72+
需要做的事:把值写回内存地址。
73+
74+
**基本步骤**
75+
76+
1.`emitLoadValue` 分别取出值(`si.getValueOperand()`)和地址(`si.getPointerOperand()`)。
77+
2.`dl_.getTypeAllocSize` 拿到值类型的大小。
78+
3. 如果大小是 4 字节,调用 `emitVStore32`;否则调用 `emitVStore`
79+
80+
与 load 的宽度选择保持对称。

docs/task5_doc/overview.md

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,124 @@
1-
该部分建设中
1+
# 整体介绍
2+
3+
本文档面向 Task5 ,围绕 `EmitMIR.cpp` 中涉及的 RV64 指令展开,帮助你理解填空所需的指令语义。
4+
5+
## 0 环境准备
6+
请执行根目录的 `task5_setup.sh` 更新实验环境
7+
8+
## 1 任务描述
9+
10+
打开 `task/5/EmitMIR.cpp`,找到下面这对标记:
11+
12+
```cpp
13+
/*
14+
-------------------------------------------------------------
15+
TASK 5 START
16+
-------------------------------------------------------------
17+
*/
18+
19+
...(需要你实现的函数)
20+
21+
/*
22+
-------------------------------------------------------------
23+
TASK 5 END
24+
-------------------------------------------------------------
25+
*/
26+
```
27+
28+
在这两个标记之间有四个函数,每个函数体里都是一句 `llvm::report_fatal_error("TODO: Student Implementation")`,你需要把它们替换成正确的实现:
29+
30+
31+
| 函数 | 职责 |
32+
| --------------- | ----------------------------------------- |
33+
| `emitBinary` | 把 LLVM IR 的二元运算(加减乘除余、位运算、移位)翻译成 RV64 MIR |
34+
| `emitICmpInst` | 把 LLVM IR 的整数比较翻译成 0/1 结果 |
35+
| `emitLoadInst` | 把 LLVM IR 的 load 翻译成 LD 或 LW |
36+
| `emitStoreInst` | 把 LLVM IR 的 store 翻译成 SD 或 SW |
37+
38+
39+
这四个函数以外的代码已经写好,包括函数序言/尾声、分支跳转、函数调用、PHI 处理、GEP 地址计算等。你不需要修改标记范围外的任何内容。
40+
41+
每个函数上方都有详细的注释提示,描述了推荐的实现步骤和需要注意的边界情况。建议先通读一遍这些注释再动手。
42+
43+
填空完成后,框架会把你生成的 MIR 交给寄存器分配器和汇编输出器,最终产出 `.s` 汇编文件。评测只看最终二进制的运行结果是否正确,不限制你选择哪些具体指令。
44+
45+
## 2 LLVM 后端导读
46+
47+
在动手填空之前,值得花几分钟理解整条后端流水线是怎么串起来的。不需要记住所有细节,但知道"我填的代码在哪个阶段、上游给了什么、下游期望什么"会让实现更有方向感。
48+
49+
### 2.1 从 LLVM IR 到汇编:全景
50+
51+
一段 C 源码经过前端和中端处理后,到达后端时是 LLVM IR 的形式。后端要做的事情可以粗略分成四步:
52+
53+
```
54+
LLVM IR → 指令选择 → 寄存器分配 → 汇编输出
55+
(需要填写) (框架已实现) (框架已实现)
56+
```
57+
58+
1. **指令选择(Instruction Selection)** — 把平台无关的 IR 指令翻译成平台相关的机器指令。比如 LLVM IR 里的 `add i32 %a, %b` 会变成 RV64 的 `ADD` 指令。这一步操作数还是虚拟寄存器,不涉及物理寄存器的分配。你要填的四个函数就在这一步。
59+
2. **寄存器分配(Register Allocation)** — 把虚拟寄存器映射到物理寄存器(a0–a7、t0–t6、s0–s11 等)。如果虚拟寄存器的数量超过了物理寄存器的数量,分配器会把一些值溢出(spill)到栈上。本框架用的是线性扫描分配器(`LinearScanAllocator`),已经实现好了。
60+
3. **汇编输出(Assembly Emission)** — 把分配好物理寄存器的 MIR 指令转成文本汇编。遇到溢出的虚拟寄存器时,输出器会自动插入 load/store 来访问栈槽。`RvInstEmitter` 负责这一步。
61+
62+
### 2.2 Machine IR 是什么
63+
64+
在本框架中,Machine IR(MIR)用 `VInst` 结构体表示。它是 LLVM IR 和最终汇编之间的中间层:
65+
66+
```
67+
LLVM IR 指令 Machine IR (VInst) 最终汇编
68+
add i32 %a, %b → ADD vreg3, vreg1, vreg2 → add a0, t1, t2
69+
```
70+
71+
一个 `VInst` 有两种角色:
72+
73+
- `**Kind::kMC**` — 包装了一条 `MCInst`(LLVM 的机器指令表示),记录了 opcode 和操作数。这是最常见的类型,你填空时生成的大部分指令都是这种。
74+
- `**Kind::kCall` / `kRet` / `kBrUncond` / `kBrCond**` — 控制流相关的高层抽象。它们不直接对应单条机器指令,而是由后面的 `RvInstEmitter` 展开成具体的参数搬运、跳转指令和函数序言/尾声。
75+
76+
### 2.3 虚拟寄存器
77+
78+
你在填空中会大量接触虚拟寄存器。它们的生命周期是这样的:
79+
80+
1. **创建** — 函数开始翻译前,`computeFrameLayoutBase` 给每个 LLVM SSA 值(参数和指令)分配一个虚拟寄存器,同时在栈上预留一个溢出槽。
81+
2. **使用** — 你在 `emitBinary` 等函数中,通过 `vregOf(&inst)` 拿到指令对应的虚拟寄存器作为目标,通过 `emitLoadValue(operand)` 拿到操作数对应的虚拟寄存器。
82+
3. **分配**`LinearScanAllocator::allocate` 遍历当前基本块的所有 VInst,建立活跃区间,然后做线性扫描分配。当前实现采用"全部溢出"策略(`forceSpillAll_ = true`),即每个虚拟寄存器都通过栈槽来传递值。
83+
4. **输出**`RvInstEmitter::emit` 在输出每条指令时,把虚拟寄存器替换成物理寄存器(或插入 load/store 来访问栈槽)。
84+
85+
你不需要操心第 3、4 步,但理解这个流程有助于解释"为什么我的代码看起来在用无限个寄存器却能跑通"。
86+
87+
### 2.4 emitMC — 生成 MIR 的统一入口
88+
89+
所有指令选择最终都调用 `emitMC`
90+
91+
```cpp
92+
void emitMC(unsigned opcode, SmallVector<MCOperand, 4> ops, std::string sym = {});
93+
```
94+
95+
它做三件事:创建一个 `VInst`,设好 opcode 和操作数,然后 push 到当前基本块的指令列表 `insts_` 里。
96+
97+
框架在 `emitMC` 之上封装了一系列 `emitV*` 辅助函数(`emitVAdd`、`emitVSub` 等),每个对应一条 RV64 指令。填空时直接调用这些辅助函数就行,不需要自己拼 MCOperand。
98+
99+
### 2.5 整体调用链
100+
101+
把上面的内容串起来,一个函数的翻译流程是:
102+
103+
```
104+
emitFunction(f)
105+
106+
├─ emitPrologue() # 输出函数序言(直接写汇编)
107+
├─ spillIncomingArgs(f) # 把参数寄存器写回栈槽
108+
109+
└─ 对每个 BasicBlock:
110+
├─ emitInst(inst) # 按指令类型分发
111+
│ ├─ emitBinary() ← 你填的
112+
│ ├─ emitICmpInst() ← 你填的
113+
│ ├─ emitLoadInst() ← 你填的
114+
│ ├─ emitStoreInst() ← 你填的
115+
│ ├─ emitCallInst() # 已实现
116+
│ ├─ emitReturnInst() # 已实现
117+
│ ├─ emitBranchInst() # 已实现
118+
│ └─ ...
119+
120+
├─ ra.allocate(insts_) # 寄存器分配
121+
└─ emitter.emit(insts_) # 输出汇编
122+
```
123+
124+
你填的四个函数产出的 VInst 会被收集到 `insts_` 里,然后统一交给寄存器分配和汇编输出。每个基本块独立走一轮这个流程。

0 commit comments

Comments
 (0)