CS110 计算机体系结构 3 RISC-V (上)
从这一章开始,我们正式深入 RISC-V 这门汇编语言,代入计算机的视角,看机器是如何把 C 代码一步步解读为二进制指令的。
RISC-V 语言简介
RISC 是一种指令集体系结构 ISA,全称为精简指令集计算机 Reduced Instruction Set Computer,而常见的 ISA 还包括 CISC(复杂指令集计算机 Complex Instruction Set Computer,早期计算机架构),X86(Intel 使用),ARM(苹果、华为使用)等。而 RISC-V 是 UC Berkeley 在 2010 年启动的开源项目,有较强的可扩展性,因此本课程采用 RISC-V RV32I 为范例。

说人话,就是 RISC-V 是一种常用于教学的汇编语言,是编译器的产出物,需要使用汇编器来解码——处于 CALL 中的 C 和 A 之间的语言。
RISC-V 基础语法
作为计算机汇编语言的一种,RISC-V 代码的所有指令都是面向指令、内存或寄存器的,会清晰、具体地告诉计算机,应该把哪个值从哪里取出来、做什么处理、再存到哪里去。注意,区别于常用的高级语言(如 C,C++,Python 等),RISC-V 是没有变量的概念的,一切数据如果不存储下来,就会被丢弃。其实高级语言的变量就是一种对于寄存器存储的封装,而汇编语言就是解决封装内部问题的地方。
RISC-V 每一行都是一条指令,使用 # 开头来标记注释——这一点和 Python 很像,但是鉴于 RISC-V 灾难般的可读性,只有写注释才能保证明天你还能看懂今天写的代码。
在“没收”了变量这一工具后,RISC-V 提供了 32 个可供使用的寄存器(本质上是虚拟寄存器,一对多地映射到部分物理寄存器上),分别命名为 x0 x1 … x31。其中,x0 寄存器是不可修改的,它的值始终为 0。在这些寄存器之外,有一个特殊的寄存器 PC(程序计数器 Program Counter),记录了程序执行到位置的首地址。
RISC-V RV32I 的语句根据功能和编码分为 6 个类别:R-Type, I-Type, S-Type, B-Type, U-Type, J-Type。我们一类类来看。
R-Type
R-Type 其中的 R 指的是 Register,即 R-Type 指令是对寄存器直接操作的指令。一般的寄存器计算都属于 R-Type 指令。
RISC-V 的 R-Type 指令一般写成 符号 目标寄存器,寄存器1,寄存器2 的形式,具体所有的 R-Type 指令如下:
加法:
add rd, rs1, rs2add x5, x2, x1把x1和x2寄存器内的值直接相加,并存储在寄存器x5内- 有进位;会忽略溢出位
减法:
sub rd, rs1, rs2sub x5, x2, x1把x2 - x1的值存入x5中
逻辑操作:
and/or/xor rd, rs1, rs2xor x6, x1, x5把x1 ^ x5存入x6中- 通过与
0x FFFF FFFF取异或来实现取反
比较:
slt/sltu rd, rs1, rs2(set if less than for signed/unsigned)slt x5, x2, x1用有符号类型(2’s complement)解读,如果x2 < x1,则把x5设为1,否则设为0sltu x6, x5, x3用无符号类型(直接读数)解读,如果x5 < x3,则把x6设为1,否则设为0
移位操作:
sll/srl/sra rd, rs1, rs2(shift left/right logic/arithmetic)sll x5, x2, x4把x2左移x4位,并存入x5中srl x6, x1, x4把x1右移x4位,用0填充左侧高位,并存入x6中sra x7, x3, x4把x3右移x4位,用原符号位(最高位)填充左侧高位,并存入x7中移位次数范围为
[0, 31],也就是说,rs2只读取其低 5 bit 的数据,其余高位数据直接忽略
I-Type
I-Type 其中的 I 指的是 Immediate,即 I-Type 指令是操作立即数的指令。涉及立即数的很多指令都属于 I-Type,其中的立即数 imm 是 12 bit 大小的。注意,一般我们写代码的时候,立即数采用十进制书写(如果硬要写作十六进制,建议加 0x 前缀)
RISC-V 中 I-Type 是最复杂的指令类型之一,具体有如下几个指令:
- 加法:
addi rd, rs1, immaddi x5, x4, -10把x4 + (-10)存入x5中
- 类似地, 定义
andiorixorisltisltuisltui会把立即数也识别成无符号模式,因此立即数为负数会出现大数比较
- 移位:
slli/srli/srai rd, rs1, imm- 只移位
imm的最低的5位的值
- 只移位
- 加载字:
lw rd, imm(rs1)(load word)lw x1, 12(x4)从地址x4 + 12(Byte)开始,(从内存)读取连续的4 byte = 32 bit存入x1中
- 加载半字:
lh/lhu rd, imm(rs1)(load halfword for signed/unsigned)lh/lhu x1, 12(x4)从地址x4 + 12(Byte)开始,(从内存)读取连续的2 byte,以有符号(原最高位填充高位)/无符号(0填充高位)模式拓展为4 byte存入x1中
- 加载字节:
lb/lbu rd, imm(rs1)(load byte for signed/unsigned)lb/lbu x1, 12(x4)从地址x4 + 12(Byte)开始,(从内存)读取连续的1 byte,以有符号(原最高位填充高位)/无符号(0填充高位)模式拓展为4 byte存入x1中
- 跳转与链接寄存器
jalr rd, imm(rs1)(jump and link register)jalr x1, imm(ra)跳转到(ra + imm)&~1,并保存返回地址PC+4(即下一条指令的地址)到x1jalr x0, 0(ra)/ret/jr ra无条件跳转到命令计数ra,一般在函数返回时使用
S-Type
S-Type 中的 S 指的是 Smile! Sweet! Save,即 S-Type 指令是将寄存器数据储存到内存的指令。注意这里不是存储到寄存器!可以想象在编程时寄存器就是我们的工作台,而内存是手边的箱子;我们希望存储的时候,我们显然会把东西存在箱子里而非工作台上,因此 S-Type 用于将寄存器数据存入内存。
S-Type 是相对简单的一种指令类型,只有几种不同的存储长度之分:
- 保存字:
sw rs2, imm(rs1)(save word)sw x1, 12(x4)将x1的值(4 byte = 32 bit)存入内存地址x4 + 12处
- 保存半字:
sh rs2, imm(rs1)(save halfword)sh x1, 12(x4)将x1的值的低位2 byte = 16 bit存入x4 + 12处
- 保存字节:
sb rs2, imm(rs1)(save byte)sb x1, 12(x4)将x1的值的低位1 byte = 8 bit存入x4 + 12处
另外,在规范上讲,强烈建议(但是语法不强制要求)存储对齐:字保存在 4 byte 的整数倍地址上,半字保存在 2 byte 的整数倍地址上。这样,在内存搜索时,数据会更快、更方便地被找到(为什么更快?学完缓存你就明白了)。
B-Type
B-Type 相比于前几种面向数据的指令不同,B 指的是 Branch,也就是分支。B-Type 指令可以在代码中实现逻辑判断和分支循环。
- 取等跳转:
beq/bne rs1, rs2, label(branch if equal/not equal)beq x1, x2, loop如果x1 == x2,则跳转到标签loop处bne x1, x2, loop如果x1 != x2,则跳转到标签loop处
- 比较跳转:
blt/bltu/bge/bgeu rs1, rs2, label(branch if less than/greater or equal to for signal/unsignal)blt x1, x2, loop如果按有符号数值比较x1 < x2,则跳转到标签loop处
U-Type
U-Type 中的 U 指的是 Upper,是为了解决 I-Type 中 Immediate 立即数只能使用 12 位的问题的指令,主要有两种:
- 更高位立即数加 PC
auipc rd, imm[31:12](Add upper immediate to program counter)auipc x6, 0xABCDE把PC + 0xABCDE000存入寄存器x6中
- 加载更高位立即数
lui rd, imm[31:12](Load upper immediate)lui x5, 0xABCDE在寄存器x5的高20位中加载立即数,即寄存器的值增加0xABCDE000
J-Type
J-Type 中的 J 指的是 Jump,也就是说 J-Type 指令负责代码跳转和链接,它只有一种指令:
跳转与链接
jal rd, label(jump and link)jal x1, func/call func/jal func跳转到标签func,并把返回地址PC+4(即下一条指令的地址)保存到x1(即ra)jal x0, loop/j loop无条件跳转到标签loop
伪指令 Pseudo-instruction
以上内容是 RISC-V 语法中本来就有的指令,是标准编译器可以直接翻译成二进制的代码,但在实际使用中,为了编写方便,还存在一些伪指令 Pseudo-instruction,而从这些伪指令翻译到指令又有标准的拓展方式,因此这部分也请和以上的语法格式一并记录在 Cheatsheet 上!
nop无操作- 拓展成
addi x0, x0, 0
- 拓展成
j loop无条件跳转- 拓展成
jal x0, loop
- 拓展成
jr rd跳转到rd- 拓展成
jalr x0, 0(rd)
- 拓展成
jal func跳转到标签func,并把返回地址PC+4(即下一条指令的地址)保存到x1- 拓展成
jal x1, func
- 拓展成
call func调用函数(同上);可近可远- 拓展成
auipc ra, offset[31:12]jalr ra, offset[11:0](ra)
- 拓展成
tail func尾调用优化,不保存返回地址,保持返回地址仍为父级返回地址;可近可远- 拓展成
auipc t1, offset[31:12]jalr x0, offset[11:0](t1)
- 拓展成
not rd, rs按位取反- 拓展成
xori rd, rs, -1
- 拓展成
beqz rs, func等于0则分支跳转- 拓展成
beq rs, x0, func
- 拓展成
bgt rs1, rs2, func大于则分支跳转- 拓展成
blt rs2, rs1, func
- 拓展成
ret函数返回到返回地址ra处- 拓展成
jalr x0, 0(ra)
- 拓展成
li rd, imm加载立即数(自动处理过大的立即数)li x5, 0xABCDE112拓展成lui x5, 0xABCDEaddi x5, x5, 0x112li x5, 0xDEADBEEF拓展成lui x5, 0xDEADCaddi x5, x5, 0xEEF(低12位最高位拓展为0xFFFFFEEF,要高位补1)
la x1, test_input把变量test_input的地址(可近可远)存入寄存器x1中- 拓展成
auipc x1, offset[31:12]addi x1, x1, offset[11:0]
- 拓展成
mv rs1, rs2将寄存器rs2的值赋值给rs1- 拓展成
addi rs1, rs2, 0
- 拓展成
调用规范 Calling Convention
RISC-V 的语法规则非常简单,而这会导致非常“多样性”的编程习惯,可能导致不同程序员写出来的汇编代码出现各种兼容性问题——因此我们需要一个调用规范。
寄存器
在调用规范中规定了不同寄存器的作用以及别名,大致分为 zero, ra, sp, t[emporary], s[aved], a 这几种,对照表如下:

在你的 Cheatsheet 上试着整理出寄存器编号-别名-管理者的速查表吧!
调用函数
在 RISC-V 中,调用一个函数的抽象过程如下:
- 把参数放在函数可以获取的位置
- 把控制权交给函数
- 获取函数必需的本地存储资源
- 函数运行
- 把函数返回值放在调用代码可以获取的位置
- 把控制权返还给调用者代码
更具体地说,就是:
- Caller 先把参数放在
a0-a7里(如果寄存器不够,开栈区空间存储参数) - 如有必要,Caller 把 caller-saved 寄存器的值压入栈区中
- 使用
jal/jalr来跳转到 Callee 函数,此时ra被设置为 Caller 的下一行指令的地址 - 改变
sp来把 callee-saved 寄存器的值压入栈区 - 如有必要,改变
sp来把本地需要存储的变量压入栈区 - Callee 函数执行
- 把返回值放在
a0-a1中 - 把 callee-saved 寄存器的值从栈区弹出,恢复寄存器的值
- 返回Caller调用点
ra - Caller 恢复 caller-saved 寄存器的值
其中,函数必需的本地存储资源就包括函数内用到的所有寄存器,而谁管理的寄存器,谁就要负责在函数调用结束后把寄存器的值恢复原状。那对于需要占用的寄存器,里面的旧值怎么处理呢?当然是上转转暂存入内存中进程的栈区。因此,你会看到在函数开始的地方,函数疯狂地往栈顶压入 s 寄存器的值,而函数即将返回的地方,函数又会把这些寄存器的值从栈区弹栈还原回去。我们来看个例子:
1 | int Leaf(int g, int h, int i, int j) |
这个 C 函数直接写成 RISC-V 是这样的:
1 | Leaf: |
由于一个 int 是 4 byte 大小,所以我们在函数调用开头(一般被称为“前言” Prologue)把栈指针 sp 向下挪 4(栈是从高地址向低地址生长的),并把 s1 存放在新开辟的地址处。为了方便起见,我们也可以使用 Caller-saved 寄存器 t1 来代替 Callee-saved 寄存器 s1,以省去复杂的压栈弹栈过程:
1 | Leaf: |
至此,我们的 RISC-V 就已经完成一大半了。具体地说,我们已经手动地模拟了编译器的工作——把 C 代码翻译为 RISC-V 汇编代码。剩下的部分,也就是下一节,我们会模拟汇编器的工作——把 RISC-V 汇编代码汇编为二进制文件。电影里经常给黑客的电脑配上一个一大串01的背景,学完下一章,我们就也能手动地把一段代码翻译为二进制01串了!
- 标题: CS110 计算机体系结构 3 RISC-V (上)
- 作者: aaaaa
- 创建于 : 2026-08-03 17:00:00
- 更新于 : 2026-08-03 22:06:26
- 链接: https://redefine.ohevan.com/2026/08/03/零基础速通系列/CS110 计算机体系结构/零基础速通:CS110_计算机体系结构_3/
- 版权声明: 版权所有 © aaaaa,禁止转载。