4. RISC-V 处理器模型解析:QEMU 中的 CPU 核心实现
好,我们进入第四章。这一章我打算把 QEMU 里 RISC-V 处理器的核心模型拆开来讲。说白了,就是看看 QEMU 怎么模拟一个 RISC-V CPU,它的特权级怎么切换,异常怎么处理,还有那个物理内存保护(PMP)和向量扩展到底是怎么一回事。
我个人觉得,理解这部分是后面做性能分析和故障定位的基础。你想想看,如果连 CPU 怎么跑起来的都不清楚,出了问题你怎么定位?
4.1 QEMU 中 RISC-V CPU 核心实现
QEMU 的 RISC-V 模拟,核心在 target/riscv/cpu.c 和 cpu.h 这两个文件里。我刚开始看的时候,也觉得代码量挺大,但抓住几个关键结构体就好理解了。
核心数据结构: CPURISCVState 结构体保存了所有寄存器状态,包括通用寄存器、PC、CSR 等。
QEMU 用 riscv_cpu_realize 函数来“实例化”一个 CPU。这个过程会初始化寄存器、设置 CSR 的默认值、配置 PMP 和向量扩展。我记得有一次调试一个启动问题,发现 CPU 的 misa 寄存器(ISA 特性寄存器)没设置对,导致某些指令无法识别。嗯,从那以后我每次都会检查 misa 的初始化。
// 简化示例:CPU 初始化流程
static void riscv_cpu_realize(DeviceState *dev, Error **errp)
{
RISCVCPU *cpu = RISCV_CPU(dev);
CPURISCVState *env = &cpu->env;
// 设置 CSR 寄存器
riscv_csr_init(env);
// 配置 PMP
if (cpu->cfg.pmp) {
riscv_pmp_init(env);
}
// 配置向量扩展
if (cpu->cfg.vext_spec) {
riscv_v_enable(env);
}
// 设置异常处理入口
env->mtvec = DEFAULT_MTVEC;
}
这里有个关键点:QEMU 的 CPU 模型是“解释执行”的,每条指令都会经过译码和执行阶段。性能分析时,你会发现某些指令(比如向量指令)的模拟开销特别大。
4.2 特权级与异常处理模型
RISC-V 的特权级,说白了就是三种模式:U(用户)、S(监督)、M(机器)。QEMU 里用 env->priv 这个字段来记录当前模式。
| 特权级 | 编码 | 主要用途 |
|---|---|---|
| U (User) | 0 | 运行应用程序 |
| S (Supervisor) | 1 | 运行操作系统内核 |
| M (Machine) | 3 | 运行固件或监控器 |
异常处理这块,QEMU 用 riscv_cpu_do_interrupt 函数来响应异常。它会根据异常类型和当前特权级,跳转到对应的异常向量(mtvec 或 stvec)。
我的经验: 调试异常处理时,我习惯在 riscv_cpu_do_interrupt 函数里加打印,看看异常原因和返回地址。这样能快速定位是哪里触发了异常。
异常处理流程大致是这样的:
- CPU 执行指令时发现异常(比如非法指令、缺页)。
- QEMU 调用
riscv_cpu_do_interrupt。 - 保存当前 PC 到 mepc/sepc,设置异常原因到 mcause/scause。
- 根据异常向量地址,跳转到处理程序。
- 处理完成后,通过 mret/sret 指令返回。
我曾经遇到过一个坑:在 S 模式下处理缺页异常时,如果异常处理程序本身又触发了缺页,就会陷入死循环。所以,异常处理程序一定要保证在 TLB 命中或直接映射的内存区域运行。
4.3 物理内存保护(PMP)机制
PMP 是 RISC-V 用来保护物理内存的机制。说白了,就是给不同特权级设置内存访问权限。QEMU 里用 pmp_entry_t 结构体来存储每个 PMP 条目。
// PMP 条目结构
typedef struct {
target_ulong addr_reg; // 地址寄存器
target_ulong cfg_reg; // 配置寄存器
} pmp_entry_t;
PMP 的配置在 pmpcfg CSR 里,每个条目有 8 位配置:
- R:读权限
- W:写权限
- X:执行权限
- A:地址匹配模式(OFF、TOR、NA4、NAPOT)
- L:锁定标志(锁定后只有 M 模式能修改)
注意: PMP 的匹配规则是从条目 0 开始,按优先级匹配。如果所有条目都不匹配,默认行为取决于 M 模式是否启用 PMP。我曾经见过一个案例,因为 PMP 条目顺序配置错误,导致 S 模式无法访问某些内存区域,系统直接崩溃。
QEMU 在每次内存访问时都会检查 PMP。检查函数是 pmp_hart_has_privs,它会遍历所有 PMP 条目,找到匹配的条目后检查权限。
4.4 向量扩展支持
RISC-V 向量扩展(V 扩展)是 1.0 版本了,QEMU 支持得还不错。向量扩展的核心是向量寄存器(v0-v31)和向量长度寄存器(vl)。
QEMU 里用 target/riscv/vector_helper.c 来实现向量指令的模拟。每个向量指令都会调用对应的 helper 函数。
// 向量加法指令的 helper 函数示例
void HELPER(vadd_vv)(CPURISCVState *env, uint32_t vm, uint32_t rs1, uint32_t rs2, uint32_t rd)
{
// 获取向量长度
uint32_t vl = env->vl;
// 逐元素计算
for (uint32_t i = 0; i < vl; i++) {
// 读取源向量寄存器元素
uint64_t a = env->vreg[rs1].u64[i];
uint64_t b = env->vreg[rs2].u64[i];
// 执行加法
env->vreg[rd].u64[i] = a + b;
}
}
性能方面,向量指令的模拟开销比标量指令大得多。我做过测试,一个简单的向量加法,QEMU 模拟时间可能是标量加法的 10 倍以上。所以,在做性能分析时,要特别关注向量指令的占比。
关键点: 向量扩展的模拟效率,很大程度上取决于向量长度(VLEN)和元素宽度(SEW)。VLEN 越大,模拟开销越大。
好了,这一章的内容就到这里。我们下一章会讲 QEMU 的内存子系统,包括地址翻译和 TLB 模拟。