3. QEMU监控器与调试接口

调试RISC-V系统,说白了就是跟硬件和模拟器打交道。QEMU提供了三把利器:监控器、GDB远程调试、日志跟踪系统。这三样东西用熟了,定位问题就像开了上帝视角。

我个人习惯,先把监控器玩透,再上GDB,最后用日志系统做兜底。顺序别搞反了,不然容易抓瞎。

QEMU RISC-V 调试接口体系 QEMU 调试引擎 QEMU Monitor GDB 远程调试 日志 & 跟踪系统 设备信息查询 内存/寄存器 快照与恢复 断点/单步 寄存器查看 反汇编 -d trace 选项 日志级别控制 QMP 三把利器配合使用,覆盖从硬件到软件的全链路调试

3.1 QEMU Monitor 基本命令

QEMU Monitor 是跟模拟器内核直接对话的接口。启动时加 -monitor stdio 就能在终端里用。我刚开始用的时候,总觉得这玩意儿就是个控制台,后来才发现它比我想象的强太多。

启动方式:

qemu-system-riscv64 -machine virt -kernel my_kernel.elf \
    -monitor stdio \
    -serial mon:stdio

进到 Monitor 后,常用的命令就这几类:

命令作用我常用的场景
info qtree查看设备树确认RISC-V外设是否挂载成功
info registers查看CPU寄存器检查PC指针是否跑飞
xp /16x 0x80000000物理内存查看验证DDR初始化后的数据
system_reset软复位不用重启QEMU就能重跑
savevm / loadvm快照保存/恢复调试到一半想回退,太香了
stop / cont暂停/继续配合单步调试用

我的小技巧: 用 info mtree 看内存布局,RISC-V的MMIO地址经常跟文档对不上,这命令一跑就现原形。

举个例子,你怀疑某个外设没映射对:

(qemu) info mtree
address-space: memory
  0000000000000000-0000000000000fff (prio 0, ram): riscv.virt.pflash
  0000000000100000-0000000000100fff (prio 0, i/o): riscv.sifive.uart
  0000000010000000-0000000010000fff (prio 0, i/o): riscv.clint

看到没?UART在0x100000,CLINT在0x10000000。如果驱动里写错了地址,这里一眼就能揪出来。

3.2 GDB 远程调试连接

GDB远程调试是重头戏。QEMU内置了GDB stub,启动时加 -s 或 -gdb tcp::1234 就能连上。

注意: -s 是 -gdb tcp::1234 的简写,默认端口1234。生产环境别用这个,容易被别人连进来搞破坏。

连接步骤很简单:

  1. 启动QEMU:qemu-system-riscv64 -machine virt -kernel app.elf -s -S
  2. 启动GDB:riscv64-unknown-elf-gdb app.elf
  3. 在GDB里连:(gdb) target remote localhost:1234
  4. 开始调试:(gdb) continue

-S 参数让CPU在启动时暂停,等你下好断点再跑。这个习惯我建议你养成,不然代码跑飞了都不知道从哪开始查。

调试RISC-V内核时,我最常用的GDB命令:

命令说明示例
info registers看所有寄存器检查 ra, sp, pc
break *0x80000000在物理地址下断点Bootloader入口
stepi单步执行一条指令跟踪异常处理流程
monitor info registers在GDB里调Monitor看QEMU内部状态
disassemble /r反汇编并显示机器码确认指令编码是否正确

避坑指南: 我曾经在调试RISC-V的S-mode代码时,发现GDB单步总是跳不对。后来才发现,QEMU默认跑在M-mode,GDB的PC指针跟实际hart的mepc对不上。解决办法是:

(gdb) monitor info registers
  ...
  mepc   0x80000100
  (gdb) set $pc = 0x80000100

手动把PC设成mepc的值,再继续单步就正常了。

3.3 QEMU 日志与跟踪系统

日志系统是调试的最后一层防线。当Monitor和GDB都搞不定的时候,就该上 -d 选项了。

启动时加 -d trace 就能看到QEMU内部的各种事件。我整理了一份常用日志选项:

日志选项输出内容适用场景
-d in_asm翻译前的目标指令确认指令是否被正确解码
-d opTCG中间操作分析指令翻译过程
-d cpu每条指令后的CPU状态跟踪寄存器变化
-d int中断事件排查中断丢失或异常触发
-d guest_errors客户机错误非法指令、地址越界
-d mmuMMU转换过程页表配置问题

举个例子,你怀疑中断没响应:

qemu-system-riscv64 -machine virt -kernel test.elf \
    -d int,cpu -D /tmp/qemu_trace.log

-D 把日志写到文件,不然终端刷屏你根本看不完。我习惯先跑一小段,然后grep关键信息:

grep -E "mip|mie|mtvec" /tmp/qemu_trace.log

高级玩法: QEMU 5.0+ 支持动态跟踪。不用重启QEMU,直接在Monitor里开日志:

(qemu) trace-event riscv_cpu_do_interrupt on
(qemu) trace-event riscv_cpu_do_interrupt off

这个功能我调试RISC-V异常处理时经常用,不用重启就能开关日志,效率高很多。

3.4 常用调试技巧

最后分享几个实战技巧,都是我在项目里踩过坑后总结的。

技巧1:多核调试别用单步

RISC-V多核系统里,GDB单步只停当前hart,其他hart还在跑。我曾经调试一个锁竞争问题,单步跟了半天发现是另一个核改了共享变量。正确做法是:

  • 用 info cpus 看所有hart的状态
  • 用 thread n 切换到目标hart
  • 在关键变量上设硬件观察点:watch *0x80001000

技巧2:用QMP做自动化调试

QEMU Machine Protocol(QMP)是JSON格式的控制接口。启动时加 -qmp tcp:localhost:4444,server,nowait,然后用Python脚本控制:

import socket, json
sock = socket.socket()
sock.connect(('localhost', 4444))
# 发送QMP命令
cmd = {"execute":"query-cpus"}
sock.send(json.dumps(cmd).encode())
resp = sock.recv(4096)
print(json.loads(resp))

这个我用来做回归测试,跑完一个用例自动复位,比手动敲命令快多了。

技巧3:结合 -d guest_errors 和 GDB

先开日志跑一遍,看有没有 guest_errors。如果有,记下出错的PC地址,再用GDB在那个地址下断点。这样定位问题比瞎猜快十倍。

我的调试三板斧:

  1. Monitor查设备状态和内存布局
  2. GDB下断点、单步、看寄存器
  3. 日志系统做全量跟踪,抓漏网之鱼

这三招用下来,90%的RISC-V调试问题都能搞定。剩下的10%,嗯,那就要看你的耐心了。