大数跨境
0
0

一次酣畅淋漓的 debug 过程:为何 NPC 与 QEMU 第一条指令不一致?

一次酣畅淋漓的 debug 过程:为何 NPC 与 QEMU 第一条指令不一致? GTOC
2026-01-07
0
导读:软件的初始化顺序很重要。当多个组件有依赖关系时,必须仔细考虑初始化顺序。


笔者在进行“一生一芯” D 线开发时,遇到了一个非常隐蔽的 bug,最终在反复 debug 后发现,问题竟然出在两行代码的执行顺序上,只是将它们调换了一下便解决了问题。


具体是在使用 difftest 功能对比 NPC 和 QEMU (REF) 的执行结果时,发现从第一条指令开始就出现寄存器不一致的错误。


主要现象是,执行 si 单步调试时,difftest 报告 t0 寄存器不一致:


(npc) si0x8000000000 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 内存中的指令:


[DEBUG] 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 -1000000001304 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 {#if defined(CONFIG_ISA_riscv) && !defined(CONFIG_RV64)    uint32_t array[33];   // riscv32: 32 GPR + PC#elif defined(CONFIG_ISA_riscv) && defined(CONFIG_RV64)    uint64_t array[65];   // riscv64: 32 GPR + 32 FPR + PC#endif};

原问题:union isa_gdb_regs 的 array[77] 对于 riscv32 来说太大,导致与 QEMU 的 GDB 协议不匹配。


修复 3:添加 -machine virt 参数

// tools/qemu-diff/include/isa.h// 修改前#define ISA_QEMU_ARGS "-bios""none",// 修改后#define ISA_QEMU_ARGS "-machine""virt""-bios""none",

这样修改,是为了确保 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   sp90x80000008: ff c1 01 13 addi    spsp-40x8000000c: 040 00 ef jal     0xf40x80000100: ff 01 01 13 addi    spsp-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


【声明】内容源于网络
0
0
GTOC
格维开源社区(GTOC),传播开源技术,布道开源文化。
内容 35
粉丝 0
GTOC 格维开源社区(GTOC),传播开源技术,布道开源文化。
总阅读11
粉丝0
内容35