RISC-V 汇编最小集
不用会写汇编程序,只要能读懂 Lab1 里那十几行,和反汇编出来的简单函数。这一页先读内存与地址。
1. CPU 一直在做的事
CPU 里有一个特殊的寄存器 pc(程序计数器),存着“下一条指令的地址”。CPU 反复做三步:从 pc 指的地址取一条指令,执行它,pc 指向下一条(RISC-V 指令大多 4 字节,所以通常是加 4)。跳转指令就是直接改 pc。
汇编就是这些指令的文字写法,一行一条。编译器把 C 翻译成汇编,汇编器再把汇编翻译成机器码(真正存在内存里的那些字节)。
2. 寄存器
寄存器是 CPU 内部的几十个小存储格,每个 64 位,读写比内存快得多。CPU 做运算只能在寄存器之间做,内存里的数要先加载到寄存器,算完再存回去。
RISC-V 有 32 个通用寄存器,编号 x0–x31。为了好记,每个都有一个按用途起的名字,反汇编里看到的是这些名字:
| 名字 | 用途 |
|---|---|
zero | 永远是 0,写进去也没用 |
ra | 返回地址:函数执行完该回到哪 |
sp | 栈指针,见第 5 节 |
tp | 线程指针。我们的内核拿它存“我是几号 CPU” |
a0–a7 | 函数参数;a0 也放返回值 |
t0–t6 | 临时用,调别的函数后可能被改掉 |
s0–s11 | 要保留的值,被调用的函数用之前得先存起来、用完还原 |
另外还有一类 CSR(控制与状态寄存器),比如 mhartid、mstatus,控制 CPU 自己的行为,只能用专门的指令读写,见特权级与异常。
3. 指令的格式
绝大多数指令长这样:操作名 目标, 源1, 源2,结果写进第一个寄存器。# 后面是注释。
add a0, a1, a2 # a0 = a1 + a2 addi a1, a1, 1 # a1 = a1 + 1 i 表示第二个源是立即数(直接写在指令里的数) mul a0, a0, a1 # a0 = a0 * a1
4. Lab1 用到的指令
| 指令 | 意思 |
|---|---|
li a0, 4096 | load immediate:a0 = 4096 |
la sp, CPU_stack | load address:sp = 符号 CPU_stack 的地址 |
mv a5, a0 | a5 = a0 |
add / addi / mul / slli | 加、加立即数、乘、左移(slli a1,a1,2 就是 a1 = a1 << 2) |
lw a4, 0(a5) | load word:从地址 a5+0 读 4 字节到 a4 |
sw a5, 0(a4) | store word:把 a5 的 4 字节写到地址 a4+0 |
ld / sd | 同上,但读写 8 字节(d 是 double word) |
beq / bne / blez | 条件跳转:相等 / 不相等 / 小于等于 0 就跳 |
j spin | 无条件跳到标号 spin |
call start | 调用函数:ra = 下一条指令的地址,然后跳到 start |
ret | 返回:跳到 ra |
csrr a1, mhartid | 读 CSR:a1 = mhartid |
csrw mepc, a0 | 写 CSR:mepc = a0 |
40(sp) 这种写法的意思是“地址 sp + 40”。括号里是基址寄存器,前面是偏移。所以 sd ra, 40(sp) 就是“把 ra 存到 sp+40 处”。
反汇编里还会看到几个“组合”:call 实际被拆成 auipc ra, ... + jalr ra 两条(先算出目标地址的高位,再跳),ret 实际是 jalr zero, 0(ra)。看到它们按 call 和 ret 理解就行。
标号、符号和伪指令
.section .text # 以 . 开头的是给汇编器的命令:下面的内容放进 .text 段 .global _entry # 让 _entry 这个名字在别的文件里也能看到 _entry: # 标号:给下一条指令的地址起个名字 la sp, CPU_stack spin: j spin # 跳回自己:原地死循环
标号和 C 里的函数名、全局变量名一样,都叫符号:一个名字对应一个地址,链接器负责把名字换成真正的地址。所以汇编里的 CPU_stack 能拿到 C 文件里定义的数组,C 里的 start 也能被汇编 call 到。
5. 栈与函数调用
函数调用有个麻烦:ra 只有一个。caller 调 leaf 时,call 会把 ra 改成“回到 caller”,那 caller 自己该回哪的地址就丢了。所以 caller 调别人之前,要把 ra 和其他要保留的寄存器存到内存里,回来后再读出来。
存这些东西的内存区域叫栈。sp 指向栈顶。RISC-V 的栈向低地址增长:函数一开始 addi sp, sp, -48,给自己腾出 48 字节;结束前 addi sp, sp, 48 还回去。局部变量放不下寄存器时,也放在这 48 字节里。
caller:
addi sp, sp, -16 # 开空间
sd ra, 8(sp) # 存 ra
call leaf # ra 被改掉了,没关系
ld ra, 8(sp) # 取回 ra
addi sp, sp, 16 # 还空间
ret # 回到调用 caller 的地方
每调一层就往下开一块,返回时收回,像一摞盘子。这就是 Lab1 里“没设 sp 就不能进 C”的原因:sp 是乱的,第一条 sd 就会写到不知道哪去。
动手:对照 C 读汇编
cd prereq/examples
make asm # 需要 RISC-V 交叉编译器,安装见 Lab0 §2
twice(x) 是 x*2+1:参数在 a0,slli a0,a0,0x1 左移一位就是乘 2,addiw 加 1,结果留在 a0,ret。编译器把乘法换成了更快的移位。
sum(a, n):a 在 a0、n 在 a1。blez a1 处理 n ≤ 0;.L3 是循环体:lw 读一个元素、addw 累加、addi a5,a5,4 指针后移 4 字节、bne 没到头就跳回去。这两个函数都不调别人,所以没碰栈。