4. RISC-V 汇编基础:寄存器组、指令格式与常用指令
好,咱们今天聊聊 RISC-V 汇编。说实话,很多同学一听到“汇编”两个字就头疼,觉得那是上古时代的东西。但我得说,搞嵌入式系统,尤其是 RISC-V 内核,汇编是绕不开的坎。你想想看,C 代码编译完是什么?就是汇编。调试器里单步执行看的是什么?还是汇编。所以,咱们把这层窗户纸捅破,后面就顺了。
4.1 寄存器组:你的“手边工具”
RISC-V 的寄存器,说白了就是 CPU 内部最快的那几个存储单元。我习惯把它们想象成工程师桌面上的工具——常用的螺丝刀、钳子、万用表,随手就能拿到。
4.1.1 通用寄存器(GPR)
RISC-V 有 32 个通用寄存器,编号从 x0 到 x31。每个都是 32 位宽(RV32)或 64 位宽(RV64)。这里有个特别的设计:x0 永远返回 0。你往 x0 写数据?没用,它还是 0。我在项目中就见过新手往 x0 里存中间结果,结果 debug 了半天——嗯,这个坑我踩过。
| 寄存器 | ABI 名称 | 用途说明 |
|---|---|---|
| x0 | zero | 硬连线为 0,常用于清零或占位 |
| x1 | ra | 返回地址,调用子程序时自动保存 |
| x2 | sp | 栈指针,指向当前栈顶 |
| x3 | gp | 全局指针,用于访问全局变量 |
| x4 | tp | 线程指针,多线程环境用 |
| x5-x7 | t0-t2 | 临时寄存器,调用者保存 |
| x8 | s0/fp | 保存寄存器,也可作帧指针 |
| x9 | s1 | 保存寄存器 |
| x10-x17 | a0-a7 | 函数参数/返回值寄存器 |
| x18-x27 | s2-s11 | 保存寄存器,被调用者保存 |
| x28-x31 | t3-t6 | 临时寄存器 |
4.1.2 控制状态寄存器(CSR)
CSR 是 RISC-V 的“控制面板”。它们不参与普通运算,但控制着 CPU 的工作模式、中断、异常等核心行为。常用的有:
- mstatus:机器状态寄存器,控制全局中断使能、特权级等
- mtvec:机器异常入口基址,告诉 CPU 出问题时跳到哪里
- mepc:异常返回地址,异常处理完后回到哪条指令
- mcause:异常原因,告诉你为什么进了异常
访问 CSR 用 csrr 和 csrw 指令。我曾经在调试一个中断响应慢的问题时,发现是 mtvec 没对齐——嗯,RISC-V 要求 mtvec 按 4 字节对齐,否则硬件会忽略低两位。这种细节,手册上写得很清楚,但你不踩一次坑,真记不住。
4.2 指令格式:六种“骨架”
RISC-V 的指令格式非常规整。所有指令都是 32 位长,分成 6 种类型。我画了个图,你一看就明白:
每种格式的字段位置是固定的,这给硬件解码带来了极大便利。我当年第一次看到这个设计时,心里就一个想法:这才叫优雅。
4.3 常用指令:干活用的“家伙什”
4.3.1 算术与逻辑指令
这些指令在 R 型和 I 型中都有。R 型操作两个寄存器,I 型操作一个寄存器加立即数。
# 算术运算
add x5, x6, x7 # x5 = x6 + x7
sub x5, x6, x7 # x5 = x6 - x7
addi x5, x6, 100 # x5 = x6 + 100
# 逻辑运算
and x5, x6, x7 # x5 = x6 & x7
or x5, x6, x7 # x5 = x6 | x7
xor x5, x6, x7 # x5 = x6 ^ x7
andi x5, x6, 0xFF # x5 = x6 & 0xFF
# 移位
slli x5, x6, 3 # x5 = x6 << 3
srli x5, x6, 2 # x5 = x6 >> 2 (逻辑右移)
srai x5, x6, 2 # x5 = x6 >> 2 (算术右移)
4.3.2 访存指令
RISC-V 的访存指令只有两种:加载(Load)和存储(Store)。Load 是 I 型,Store 是 S 型。
# 加载指令
lb x5, 0(x6) # 加载字节(有符号扩展)
lbu x5, 0(x6) # 加载字节(无符号扩展)
lh x5, 0(x6) # 加载半字
lhu x5, 0(x6) # 加载半字(无符号)
lw x5, 0(x6) # 加载字
# 存储指令
sb x5, 0(x6) # 存储字节
sh x5, 0(x6) # 存储半字
sw x5, 0(x6) # 存储字
注意:RISC-V 不支持“一次加载多个寄存器”的指令(比如 ARM 的 LDM)。这是为了简化硬件设计。我刚开始从 ARM 转过来时很不习惯,但用久了发现,少就是多。
4.3.3 分支与跳转指令
分支指令是 B 型,跳转指令是 J 型。分支是条件跳转,跳转是无条件跳转。
# 条件分支(B型)
beq x5, x6, label # if x5 == x6, 跳转到 label
bne x5, x6, label # if x5 != x6, 跳转
blt x5, x6, label # if x5 < x6 (有符号), 跳转
bge x5, x6, label # if x5 >= x6 (有符号), 跳转
bltu x5, x6, label # if x5 < x6 (无符号), 跳转
bgeu x5, x6, label # if x5 >= x6 (无符号), 跳转
# 无条件跳转(J型)
jal x1, func # 跳转到 func, 返回地址存 x1
jalr x0, x1, 0 # 从函数返回(跳转到 x1 指向的地址)
4.4 汇编程序结构:从“裸奔”到“穿衣”
一个完整的 RISC-V 汇编程序,通常包含以下几个部分:
.section .text # 代码段
.globl main # 声明 main 为全局符号
main:
# 函数序言:保存返回地址和栈帧
addi sp, sp, -16 # 分配栈空间
sw ra, 12(sp) # 保存返回地址
# 你的代码写在这里
li a0, 42 # 加载立即数 42 到 a0
jal print_number # 调用子函数
# 函数尾声:恢复现场并返回
lw ra, 12(sp) # 恢复返回地址
addi sp, sp, 16 # 回收栈空间
ret # 返回
.section .data # 数据段
msg: .asciz "Hello" # 定义一个字符串
.section .bss # 未初始化数据段
.lcomm buffer, 256 # 分配 256 字节缓冲区
这个结构,说白了就是“三段式”:代码段放指令,数据段放初始化的全局变量,bss 段放未初始化的全局变量。我在项目中见过有人把所有东西都塞到 .text 段里,结果链接时报了一堆地址冲突——嗯,该分还是得分。
好了,关于 RISC-V 汇编的基础,咱们就聊到这儿。寄存器、指令格式、常用指令、程序结构,这四个点串起来,就是汇编编程的“四梁八柱”。你把这些搞明白了,后面看启动代码、看中断处理,就会觉得——哦,原来就这么回事。