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 函数里加打印,看看异常原因和返回地址。这样能快速定位是哪里触发了异常。

异常处理流程大致是这样的:

  1. CPU 执行指令时发现异常(比如非法指令、缺页)。
  2. QEMU 调用 riscv_cpu_do_interrupt。
  3. 保存当前 PC 到 mepc/sepc,设置异常原因到 mcause/scause。
  4. 根据异常向量地址,跳转到处理程序。
  5. 处理完成后,通过 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 模拟。

QEMU RISC-V CPU 核心模型结构 CPU 核心 特权级管理 U / S / M 模式切换 异常处理 mtvec / stvec 跳转 PMP 保护 物理内存权限检查 向量扩展 (V) 核心关系说明 CPU 核心通过特权级管理控制异常处理和 PMP 保护 向量扩展作为可选特性,与核心紧密集成