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 临时寄存器 否
我的习惯:在逆向分析时,我会特别关注 a0-a7(参数传递)和 ra(返回地址)。如果 ra 被篡改,那基本就是栈溢出攻击。另外,sp 的值也很关键,它决定了栈帧的布局。

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]
关键点:所有指令的 opcode 都在最低 7 位,rs1 和 rs2 的位置也基本固定。这让你在逆向分析时,即使只看二进制数据,也能快速判断出指令类型。我曾经在分析一个被混淆的固件时,就是靠这个规律手动还原了大部分指令流。

4.3 特权级与异常处理

RISC-V 定义了三个特权级:机器模式(M 模式)、主管模式(S 模式)、用户模式(U 模式)。M 模式是最高权限,可以访问所有资源;S 模式通常用于操作系统内核;U 模式用于运行用户程序。

为什么要有特权级?说白了就是为了隔离和保护。用户程序不能随便访问硬件寄存器,也不能随意切换页表。一旦它越界,CPU 就会触发异常,切换到更高特权级来处理。

4.3.1 异常与陷阱

在 RISC-V 里,异常(Exception)和中断(Interrupt)统称为陷阱(Trap)。异常是同步的,比如执行了非法指令、访问了未对齐的地址;中断是异步的,比如定时器到期、外部设备发信号。

当陷阱发生时,CPU 会做以下事情:

  1. 暂停当前指令流。
  2. 将当前 PC 保存到 mepc(或 sepc)。
  3. 将陷阱原因写入 mcause(或 scause)。
  4. 根据 mtvec(或 stvec)跳转到处理程序。
  5. 切换特权级到 M 模式(或 S 模式)。

处理程序执行完毕后,通过 mret 或 sret 指令返回。这时 CPU 会恢复 mepc 到 PC,并恢复之前的特权级。

避坑指南:我曾经在调试一个 RISC-V 内核时,发现异常处理程序总是进不去。查了半天,原来是 mtvec 的值没有按 4 字节对齐。RISC-V 要求陷阱向量地址必须是 4 的倍数,否则 CPU 会直接忽略这个设置。嗯,这种低级错误,排查起来真的很费时间。

4.3.2 异常处理流程

下面这张图展示了从异常发生到处理完成的完整流程:

RISC-V 异常处理流程 RISC-V 异常处理流程 正常指令执行 异常/中断发生 保存 PC 到 mepc 写入异常原因到 mcause 根据 mtvec 跳转 切换到 M 模式 执行异常处理程序 mret 返回 关键寄存器 mepc: 异常 PC mcause: 异常原因 mtvec: 处理程序入口 mstatus: 状态控制

4.4 在 Pwn 中的实际意义

你可能会问,这些基础知识和 Pwn 有什么关系?关系大了。我简单举几个例子:

  • ROP 利用:你需要知道 ra 寄存器保存返回地址,所以控制栈上 ra 的位置就能劫持控制流。另外,a0-a7 是参数传递寄存器,构造 ROP 链时要把参数放到正确的位置。
  • 异常处理劫持:如果你能修改 mtvec 或 stvec,就能让 CPU 在发生异常时跳转到你的 shellcode。这在某些嵌入式设备上非常实用。
  • 指令编码分析:理解指令格式后,你就能手动 patch 二进制文件。比如,把一条 beq 改成 bne,只需要修改 funct3 字段的几位。
  • 特权级提升:如果你在 U 模式下发现了漏洞,可以通过修改 mstatus 的权限位来提升到 M 模式。当然,这通常需要配合其他漏洞。
我的建议:刚开始接触 RISC-V 逆向时,不要急着看复杂的指令。先把寄存器用途和指令格式背熟,然后找几个简单的固件练手。我记得我第一次成功分析出一个 RISC-V 的缓冲区溢出漏洞时,那种成就感真的很棒。