CS110 计算机体系结构 4 RISC-V (下)
上一章把从 C 代码翻译为 RISC-V 代码的过程作了大概的介绍,而这一章,我们就要和二进制打交道,学习如何手动把 RISC-V 代码汇编为二进制的01串。
汇编器的工作
RISC-V 代码既然是汇编语言代码,在汇编器处就应当能被直接转换为二进制。在学习二进制转换之前,我们先来了解一下汇编器的工作原理(在 CS110P 的 Project 中,你会被要求用 C 语言手动实现一个汇编器,所以请务必理解其工作原理)。
由于 RISC-V 的代码不强制要求先声明或定义后调用,因此会涉及到前向引用的问题,所以至少需要扫描两遍才能完成汇编。我们把这个过程叫做 Two-Pass。
在第一遍扫描中,我们需要把不能直接翻译成二进制的问题中的大部分都解决。由于跳转指令是跳转至指定的标签处,而标签的位置是我们的代码决定的,因此首先需要完成的工作就是把代码中所有的标签地址存入一张符号表中,方便后续跳转指令直接读取地址。那么在这个过程中,我们就涉及到一个地址的概念。在一个程序中,我们认为第一句指令的基地址是 0x0000 0000,然后后面每句语句(以 32 位的 RISC-V 代码为例)在这个基础上 +4,即第二句基地址就是 0x0000 0004。注意,在这个过程中,我们只会数指令的数量,不包括标签、伪指令、注释这些内容。那么,聪明的读者应该已经意识到了,在存这张符号表之前,我们应该一路展开遇到的所有伪指令,事实的确如此。以上这两件事情都是可以在第一遍扫描中完成的。我们举个例子方便说明:
1 | # 这是一个简单的减法循环 |
我们从头开始对上述代码进行 Pass One 的扫描:
# 这是一个简单的减法循环
- 处理:忽略注释(后续注释已经自动忽略了)。
- PC 不变 (
0x0000 0000)。start:
- 处理:发现标签。记录
{start: 0x0000 0000}到符号表。- PC 不变。
li t0, 2
- 处理:这是伪指令。由于 2 很小,它被展开为
addi t0, x0, 2(1条指令)。- PC 增加 4 -> 下个地址为
0x0000 0004。li t1, 0
- 处理:伪指令。展开为
addi t1, x0, 0(1条指令)。- PC 增加 4 -> 下个地址为
0x0000 0008。loop:
- 处理:发现标签。记录
{loop: 0x0000 0008}到符号表。- PC 不变。
beq t0, t1, done
- 处理:标准指令。此时不知道
done的地址,但没关系,我们先确定这条指令占 4 字节。- PC 增加 4 -> 下个地址为
0x0000 000C。addi t0, t0, -1
- 处理:标准指令。
- PC 增加 4 -> 下个地址为
0x0000 0010。j loop
- 处理:伪指令。展开为
jal x0, loop(1条指令)。- PC 增加 4 -> 下个地址为
0x0000 0014。done:
- 处理:发现标签。记录
{done: 0x0000 0014}到符号表。- PC 不变。
mv a0, t0
- 处理:伪指令。展开为
addi a0, t0, 0(1条指令)。- PC 增加 4 -> 下个地址为
0x0000 0018。ret
- 处理:伪指令。展开为
jalr x0, 0(x1)(1条指令)。- PC 增加 4 -> 下个地址为
0x0000 001C。这样,我们的 Pass One 扫描就结束了,得到一张填好的符号表和一段已经展开为指令的代码。
在第二遍扫描的内容非常简单:就是把所有的处理后的指令直接翻译为二进制。具体如何转换为二进制,我们单独介绍。
RISC-V 指令的二进制编码


以上编码表均摘自绿卡 riscvcard.pdf,在每次期中、期末考试时都会人手一张发下来,不过如果 Cheatsheet 上有空地的话,还是建议手抄一份,这样可以大幅度避免考试的时候翻来翻去查表。粗看这张表,不难发现,B-Type 的编码格式和 S-Type 很像,因此也作 SB-Type;J-Type 的编码格式和 U-Type 很像,因此也作 UJ-Type。不过,这部分属于旧称,正式使用时还请使用 B-Type,J-Type 这样的名称。
现在我们来了解一下编码表中的不同符号表示什么:
funct7/funct3都是用于区分同类型的不同指令的,数字 7 和 3 分别表示这段代码的长度为 7 或 3。这个部分可以从绿卡中查到,不用自己记忆或者抄在 Cheatsheet 上。rs1/rs2/rd都是代表寄存器 Register,而rs是来源寄存器 Source(如加法的加数、减法的减数或被减数存放的寄存器),rd是目的寄存器 Destination(保存运算结果的寄存器)。这些寄存器都是5位长度的,代表寄存器的编号(如a0是x10的别名,因此编码为 10,也就是01010)opcode是用于区分不同类型指令的,每种类型的指令都有不同的 opcode,但是 I-type 和 U-type 有多种 opcode。具体的内容可以查表,顺手抄在 Cheatsheet 上可以更方便查阅。imm是指令中的立即数,后面的中括号是 python 的切片表示法(注意0起编号)。如一个12位立即数-4,用二进制表示就是0b 1111 1111 1100,如果是imm[4:1]就是指1110这四位。这部分需要手动熟练十进制转二进制。
在汇编过程中,我们先按照编码表写出指令的二进制表达形式,然后四位一段翻译成十六进制数(不熟练的也可以像我一样写一张16-10进制速查表),最后写成8位十六进制的形式。
我们来举几个例子:
例:将
bge t0, s1, .Ladd翻译为十六进制表示,其中.Ladd在当前语句下方,和当前语句之间有 3 行指令。我们知道,
bge属于 B-Type 指令,因此按照对应格式编码。我们先提取出指令需要的各个部分的二进制值:
imm实际上存储的是偏移量,即跳转目标基地址 - 当前语句基地址。既然跳转目标和当前语句之间有 3 行指令,我们就知道,imm的值是,即 0 0000 0001 0000。rs1是t0,通过查表可知是x5,即00101。rs2是s1,通过查表可知是x9,即01001。- 通过查表可知,
bge的funct3是101。- 通过查表可知,B-Type 指令的
opcode都是1100011。这样我们的原材料就全了,只需要按照编码格式拼接即可,得到二进制结果为:
0000000 | 01001 | 00101 | 101 | 10000 | 1100011把它四位一段地写开,就能转换为十六进制了:
0000 0000 1001 0010 1101 1000 0110 0011即
0x0092 D863。
例:将
0x0D00 0723转换为指令表示。(很遗憾那个不是合法的指令)和上一例相反,我们先拆开成二进制:
0000 1101 0000 0000 0000 0111 0010 0011然后尝试识别最后 7 位的 opcode:
0100011,比对发现是 S-Type。所以我们按照对应的格式来“断句”解码:
0000110 | 10000 | 00000 | 000 | 01110 | 0100011我们可以提取出:
imm是0000 1100 1110,即十进制的206。rs1是00000,对应寄存器x0。rs2是10000,对应寄存器x16,别名是a6。funct3是000,对应操作是 Save Byte 的sb rs2, imm(rs1)。于是我们填入内容即可:
sb a6, 206(x0)。
需要注意的是,在考试中,给你的代码有可能包含伪指令,此时你需要先计算展开伪指令后的指令行数,再计算跳转相关指令的立即数 imm 的值。(笔者曾经因这个原因 Request Regrade,成功修改了期中考试I的答案。坚持相信你正确的答案!)
到此,我们关于 RISC-V 的内容就基本介绍完毕了。请读者考前勤加练习,熟练掌握 RISC-V 指令编写的惯例以及手动汇编为二进制表示的方法。在下一章中,我将对 C.A.L.L. 进行详细的说明,在了解 RISC-V 后,相信读者会对其有更深的理解。
- 标题: CS110 计算机体系结构 4 RISC-V (下)
- 作者: aaaaa
- 创建于 : 2026-08-08 15:00:00
- 更新于 : 2026-08-08 15:28:17
- 链接: https://redefine.ohevan.com/2026/08/08/零基础速通系列/CS110 计算机体系结构/零基础速通:CS110_计算机体系结构_4/
- 版权声明: 版权所有 © aaaaa,禁止转载。