3. QEMU监控器与调试接口
调试RISC-V系统,说白了就是跟硬件和模拟器打交道。QEMU提供了三把利器:监控器、GDB远程调试、日志跟踪系统。这三样东西用熟了,定位问题就像开了上帝视角。
我个人习惯,先把监控器玩透,再上GDB,最后用日志系统做兜底。顺序别搞反了,不然容易抓瞎。
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。生产环境别用这个,容易被别人连进来搞破坏。
连接步骤很简单:
- 启动QEMU:
qemu-system-riscv64 -machine virt -kernel app.elf -s -S - 启动GDB:
riscv64-unknown-elf-gdb app.elf - 在GDB里连:
(gdb) target remote localhost:1234 - 开始调试:
(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 op | TCG中间操作 | 分析指令翻译过程 |
-d cpu | 每条指令后的CPU状态 | 跟踪寄存器变化 |
-d int | 中断事件 | 排查中断丢失或异常触发 |
-d guest_errors | 客户机错误 | 非法指令、地址越界 |
-d mmu | MMU转换过程 | 页表配置问题 |
举个例子,你怀疑中断没响应:
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在那个地址下断点。这样定位问题比瞎猜快十倍。
我的调试三板斧:
- Monitor查设备状态和内存布局
- GDB下断点、单步、看寄存器
- 日志系统做全量跟踪,抓漏网之鱼
这三招用下来,90%的RISC-V调试问题都能搞定。剩下的10%,嗯,那就要看你的耐心了。