具体是在使用 difftest 功能对比 NPC 和 QEMU (REF) 的执行结果时,发现从第一条指令开始就出现寄存器不一致的错误。
主要现象是,执行 si 单步调试时,difftest 报告 t0 寄存器不一致:
(npc) si0x80000000: 00 00 02 97 auipc t0, 0+------+------------+------------+----------+| Difftest FAILED at PC = 0x80000000 |+------+------------+------------+----------+| Reg | REF | NPC | Status |+------+------------+------------+----------+| t0 | 0x00000000 | 0x80000000 | MISMATCH || pc | 0x80000004 | 0x80000004 | OK |+------+------------+------------+----------+
简单分析:
先看 NPC,这里 t0 = 0x80000000(执行 auipc t0, 0 后正确);再关注一下参模型 REF (QEMU),t0 = 0x00000000(错误,auipc 应该将 PC 值写入 t0)。
因此可以确定,后续指令的 t0 差异是由第一条指令的错误累积导致的。
我们来分析一下根因。
初步排查,笔者通过添加调试代码,发现:
// init_difftest 之后After init: NPC pc=0x80000000, REF pc=0x80000000 // PC 初始化正确After init: NPC t0=0x0, REF t0=0x0 // t0 初始化正确// difftest_step 中Before exec: REF pc=0x80000000, t0=0x0After exec: REF pc=0x80000004, t0=0x0 // QEMU 执行后 t0 仍为 0!NPC state: pc=0x80000004, t0=0x80000000
QEMU 的 PC 从 0x80000000 变到 0x80000004(说明确实执行了一条指令),但 t0 却没有变化。
接着深入排查一下,检查 QEMU 内存中的指令:
[] Memory at 0x80000000: QEMU=0x00000413, expected=0x00000413
简单分析:
- 0x00000413 = addi s0, x0, 0(即 li s0, 0)
- 0x00000297 = auipc t0, 0
可以发现,QEMU 执行的是 addi s0, x0, 0,而 NPC 显示执行的是 auipc t0, 0!
我们检查镜像文件来验证一下:
$ xxd add-riscv32-nemu.bin | head -100000000: 1304 0000 ... # 小端序读取为 0x00000413
用户镜像的第一条指令确实是 0x00000413(addi s0, x0, 0)。
我们再看一下 NPC 的内置镜像:
// src/isa/riscv32/init.cstatic const uint32_t img[] = {0x00000297, // auipc t0,0 <-- 内置镜像的第一条指令0x00028823, // sb zero,16(t0)...};
NPC 执行的是内置镜像(auipc t0, 0),而 QEMU 执行的是用户镜像(addi s0, x0, 0)!
所以到这里,问题基本就很明朗了,根本原因是 NPC 的初始化顺序错误。原始的初始化顺序如下:
//src/monitor/monitor.cinit_isa(); // 1. 复制内置镜像到内存npc_core_init(); // 2. 初始化 NPC 核心(复位时取指,取到 auipc t0, 0)load_img(); // 3. 加载用户镜像(覆盖内存,但 NPC 已经取好指令了!)init_difftest(); // 4. 把用户镜像复制到 QEMU
我们通过一个图表再来捋一下:
时间线:┌─────────────────────────────────────────────────────────────────────┐│ init_isa() │ 内存: [auipc t0, 0] (内置镜像) │├─────────────────────────────────────────────────────────────────────┤│ npc_core_init() │ NPC 复位,取指 → IR = auipc t0, 0 ││ │ (指令已经锁存到 NPC 的寄存器中!) │├─────────────────────────────────────────────────────────────────────┤│ load_img() │ 内存: [addi s0, 0] (用户镜像覆盖) ││ │ 但 NPC 的 IR 仍然是 auipc t0, 0! │├─────────────────────────────────────────────────────────────────────┤│ init_difftest() │ QEMU 内存: [addi s0, 0] (正确的用户镜像) │├─────────────────────────────────────────────────────────────────────┤│ 执行第一条指令 │ NPC 执行 auipc t0, 0 → t0 = 0x80000000 ││ │ QEMU 执行 addi s0, 0 → s0 = 0 ││ │ → MISMATCH! │└─────────────────────────────────────────────────────────────────────┘
关键点在于,NPC 是一个硬件模拟器(Verilator),在 npc_core_init() 复位时,CPU 会进行取指操作(IF 阶段),将 PC 指向的指令从内存读取到指令寄存器(IR)中。此时内存中还是内置镜像,所以取到的是 auipc t0, 0。
之后 load_img() 虽然覆盖了内存,但 NPC 的指令寄存器中已经锁存了旧指令,不会因为内存变化而改变。所以执行时 NPC 用的是内置镜像的指令,而 QEMU 用的是用户镜像的指令,导致不一致。
下面是笔者的修复方案,主要有三处:
修复 1:调整初始化顺序(主要修复)
// src/monitor/monitor.c// 修改后的初始化顺序init_isa(); // 1. 复制内置镜像到内存load_img(); // 2. 加载用户镜像(覆盖内置镜像)npc_core_init(); // 3. 初始化 NPC 核心(现在读取的是用户镜像)init_difftest(); // 4. 把用户镜像复制到 QEMU
修复 2:修正 QEMU riscv32 的 GDB 寄存器大小
// tools/qemu-diff/include/isa.h// 修改前struct {uint32_t array[77]; // 所有架构统一使用 77};// 修改后struct {uint32_t array[33]; // riscv32: 32 GPR + PCuint64_t array[65]; // riscv64: 32 GPR + 32 FPR + PC};
原问题:union isa_gdb_regs 的 array[77] 对于 riscv32 来说太大,导致与 QEMU 的 GDB 协议不匹配。
修复 3:添加 -machine virt 参数
// tools/qemu-diff/include/isa.h// 修改前// 修改后
这样修改,是为了确保 QEMU riscv32 使用正确的机器类型和内存映射。
修复后,difftest 正常工作:
$ echo -e "si\nsi\nsi\nsi\nsi\nq" | make run IMG=add-riscv32-nemu.bin0x80000000: 00 00 04 13 mv s0, zero # 正确执行用户镜像0x80000004: 00 00 91 17 auipc sp, 90x80000008: ff c1 01 13 addi sp, sp, -40x8000000c: 0f 40 00 ef jal 0xf40x80000100: ff 01 01 13 addi sp, sp, -0x10
下面进行经验总结:
初始化顺序很重要。当多个组件有依赖关系时,必须仔细考虑初始化顺序。在这个案例中,NPC 核心需要从内存读取指令,因此必须在用户镜像加载之后才能初始化。
Difftest 的调试方法可以总结如下:
1. 先验证初始化是否正确(PC、寄存器);
2. 检查执行前后的状态变化;
3. 验证内存中的指令是否与预期一致;
4. 对比两边执行的实际指令.
另外还需要注意 GDB 协议的架构差异,不同 ISA 的 GDB 协议返回的寄存器数量不同,必须使用正确的数据结构大小。
作者:KINGFIOX
首图:主核(Kernyr)
审校:泽文(Zevorn)
图片、资料来源:
[1] Difftest Bug 报告:NPC 与 QEMU 第一条指令不一致
-
关注公众号,免费使用社区提供的 ima 知识库
现已推出:AI Infra/QEMU/Compiler/Linux

