CPU 看似神秘,其实每时每刻只做三件事:从哪里取数、做什么运算、把结果放回哪里。而「放回哪里」的这个地方,就叫寄存器(register)——它是 CPU 自己的工作台,容量小得可怜,但速度是全机最快的。这一讲我们把工作台认全,然后写下你的第一条汇编指令。
32 个通用寄存器
RV32I 一共有 32 个通用寄存器,编号 x0 到 x31,每个 32 位宽(这就是 RV32 里的 32)。听起来很少?x86-64 有 16 个,8086 只有 8 个——RISC 设计哲学就是「寄存器多而快,编译器随便用」。
不过日常读汇编时,你几乎看不到 x5、x10 这种编号,看到的全是 ABI 名字——按用途起的外号:
| 编号 | ABI 名 | 用途 | 谁保存 |
|---|---|---|---|
| x0 | zero |
恒为零,写不进去 | —— |
| x1 | ra |
函数返回地址 | 调用者破坏 |
| x2 | sp |
栈指针 | 专用 |
| x3 | gp |
全局指针 | 专用 |
| x4 | tp |
线程指针 | 专用 |
| x5–x7, x28–x31 | t0–t6 |
临时值 | 调用者保存 |
| x8, x9, x18–x27 | s0–s11 |
需要跨函数存活 | 被调者保存 |
| x10–x17 | a0–a7 |
函数参数/返回值 | 调用者保存 |
t 是 temporary,s 是 saved,a 是 argument——先混个脸熟,第 05 讲函数调用时你会真正用到这张表。
x0:一个故意「坏了」的寄存器
zero 是整个指令集里最有意思的设计:读它永远是 0,写它等于扔进黑洞。
为什么要故意造一个坏掉的寄存器?因为 0 在程序里出现得太频繁了:清零、比较、置初值……有了恒零寄存器,这些操作全部不需要专门的指令:
addi a0, zero, 42 # a0 = 0 + 42,把常数装进寄存器
add a0, zero, a1 # 等价于「把 a1 复制到 a0」
add zero, zero, zero# 著名的 nop(什么都不做)
一套加法指令,顺手解决了「装常数」「复制」「空转」三件事——这是精简指令集「用规则换取简洁」的典型体现。
第一条指令:addi
汇编指令的通用形状是「做什么,操作数,操作数」。我们写第一条:
addi a0, zero, 42 # a0 = 0 + 42
addi= add immediate(加立即数):把一个常数加到寄存器上- 格式:
addi rd, rs1, imm——结果放rd,源是rs1加常数imm - 立即数是 12 位有符号数,范围 -2048 到 2047,超了要分两步(第 07 讲教)
再来两条热热身:
addi t0, zero, 10 # t0 = 10
addi t0, t0, -1 # t0 = t0 - 1 ——减法就是加负数
注意第二行:RV32I 里有 add 却没有 subi。想要「减常数」,加个负数就行——少一条指令,硬件就少一套电路。
动手验证
读十遍不如跑一遍。打开 HI RISC-V App 的模拟器沙盒,敲下这三行:
addi a0, zero, 42 # a0 = 42
addi a1, zero, 8 # a1 = 8
add a2, a0, a1 # a2 = 50
单步执行,盯住寄存器面板:你会看到 a2 在第三条指令后变成 50(十六进制 0x32)。改改立即数、把 zero 换成 a0,观察每条指令如何改写工作台。
免费下载 HI RISC-V App,前 5 关完全免费。
本讲三句话
- RV32I 有 32 个 32 位寄存器,汇编里用 ABI 名(
zero/ra/sp/t0/a0…) zero恒为零:装常数、复制、空转全靠它addi rd, rs1, imm是你的第一条指令,立即数范围 ±2048
下一讲我们把加减法做完,顺便回答一个有趣的问题:RISC-V 为什么没有 flags 寄存器?