4. RISC-V 架构基础:寄存器组、指令格式、特权级与异常处理
好,我们进入 RISC-V 架构。说实话,在接触 RISC-V 之前,我做了很多年 ARM 和 x86 的逆向。第一次拿到 RISC-V 的固件时,我有点懵——指令长度不固定?寄存器这么多?但真正上手后,我发现 RISC-V 的设计哲学其实非常优雅。它不像 x86 那样背负着几十年的历史包袱,也不像 ARM 那样有那么多变种。RISC-V 的核心理念就是:简洁、模块化、可扩展。
这一章,我会带你快速掌握 RISC-V 的核心基础。这些知识是后续静态分析和动态调试的基石。你想想看,如果你连寄存器是干嘛的、指令怎么编码的都不知道,那逆向分析根本无从下手。
4.1 RISC-V 寄存器组
RISC-V 的寄存器组,说白了就是 CPU 内部的一小块高速存储空间。我个人习惯把它分成三类:通用寄存器、控制状态寄存器、以及一个特殊的 PC 寄存器。
4.1.1 通用寄存器(x0 - x31)
RISC-V 有 32 个通用寄存器,每个都是 32 位(RV32)或 64 位(RV64)宽。它们被命名为 x0 到 x31。但这里有个坑:x0 是硬编码为 0 的。你往 x0 里写任何数据,它都纹丝不动,读出来永远是 0。我在项目中遇到过有人把目标地址写到了 x0 里,结果程序直接跑飞了——嗯,这种错误排查起来很痛苦。
剩下的 x1 到 x31,虽然硬件上没区别,但 ABI(应用程序二进制接口)给它们定义了不同的用途。这张表你最好记下来,逆向分析时经常要用到:
| 寄存器 | 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 的行为、记录状态。比如,你通过 CSR 来配置中断、管理内存保护、读取异常原因。
常用的 CSR 有:
- mstatus:机器模式状态寄存器,控制全局中断使能、特权级切换等。
- mtvec:机器模式陷阱向量基址寄存器,指向异常处理程序的入口地址。
- mcause:机器模式异常原因寄存器,告诉你发生了什么异常。
- mepc:机器模式异常 PC 寄存器,保存发生异常时的指令地址。
- satp:主管地址保护与翻译寄存器,用于开启分页内存管理。
访问 CSR 需要专门的指令:csrr(读)、csrw(写)、csrs(置位)、csrc(清零)。在 Pwn 利用中,如果你能控制 CSR 的值,比如修改 mtvec 指向你的 shellcode,那就能劫持异常处理流程。不过,这通常需要你在机器模式下才能操作。
4.2 指令格式
RISC-V 的指令格式是我见过最规整的。它只有六种基本格式,每种格式的指令编码位置都是固定的。这意味着解码器可以并行工作,效率很高。
这六种格式分别是:
- R 型:寄存器-寄存器操作,比如加法
add rd, rs1, rs2。 - I 型:立即数操作,比如加载
lw rd, offset(rs1)。 - S 型:存储操作,比如
sw rs2, offset(rs1)。 - B 型:条件分支,比如
beq rs1, rs2, offset。 - U 型:上立即数,比如
lui rd, imm。 - J 型:无条件跳转,比如
jal rd, offset。
每种格式的 32 位编码布局如下:
R 型: [funct7][rs2][rs1][funct3][rd][opcode]
I 型: [imm[11:0]][rs1][funct3][rd][opcode]
S 型: [imm[11:5]][rs2][rs1][funct3][imm[4:0]][opcode]
B 型: [imm[12|10:5]][rs2][rs1][funct3][imm[4:1|11]][opcode]
U 型: [imm[31:12]][rd][opcode]
J 型: [imm[20|10:1|11|19:12]][rd][opcode]
4.3 特权级与异常处理
RISC-V 定义了三个特权级:机器模式(M 模式)、主管模式(S 模式)、用户模式(U 模式)。M 模式是最高权限,可以访问所有资源;S 模式通常用于操作系统内核;U 模式用于运行用户程序。
为什么要有特权级?说白了就是为了隔离和保护。用户程序不能随便访问硬件寄存器,也不能随意切换页表。一旦它越界,CPU 就会触发异常,切换到更高特权级来处理。
4.3.1 异常与陷阱
在 RISC-V 里,异常(Exception)和中断(Interrupt)统称为陷阱(Trap)。异常是同步的,比如执行了非法指令、访问了未对齐的地址;中断是异步的,比如定时器到期、外部设备发信号。
当陷阱发生时,CPU 会做以下事情:
- 暂停当前指令流。
- 将当前 PC 保存到
mepc(或sepc)。 - 将陷阱原因写入
mcause(或scause)。 - 根据
mtvec(或stvec)跳转到处理程序。 - 切换特权级到 M 模式(或 S 模式)。
处理程序执行完毕后,通过 mret 或 sret 指令返回。这时 CPU 会恢复 mepc 到 PC,并恢复之前的特权级。
mtvec 的值没有按 4 字节对齐。RISC-V 要求陷阱向量地址必须是 4 的倍数,否则 CPU 会直接忽略这个设置。嗯,这种低级错误,排查起来真的很费时间。
4.3.2 异常处理流程
下面这张图展示了从异常发生到处理完成的完整流程:
4.4 在 Pwn 中的实际意义
你可能会问,这些基础知识和 Pwn 有什么关系?关系大了。我简单举几个例子:
- ROP 利用:你需要知道 ra 寄存器保存返回地址,所以控制栈上 ra 的位置就能劫持控制流。另外,a0-a7 是参数传递寄存器,构造 ROP 链时要把参数放到正确的位置。
- 异常处理劫持:如果你能修改 mtvec 或 stvec,就能让 CPU 在发生异常时跳转到你的 shellcode。这在某些嵌入式设备上非常实用。
- 指令编码分析:理解指令格式后,你就能手动 patch 二进制文件。比如,把一条
beq改成bne,只需要修改 funct3 字段的几位。 - 特权级提升:如果你在 U 模式下发现了漏洞,可以通过修改 mstatus 的权限位来提升到 M 模式。当然,这通常需要配合其他漏洞。