本文结合
ysyx-workbench/nemu的实际代码,讲解“踪迹”这一调试基础设施,以及它在 NEMU 中的完整实现。
摘要
程序本质上是一个状态机:
S0 -> S1 -> S2 -> S3 -> ...当程序复杂、状态数量变多时,仅靠观察最终状态很难定位 bug。
踪迹(Trace)的作用就是:
记录状态机运行过程中的关键事件 出问题后回溯最近发生了什么NEMU 中常见的踪迹包括:
ITRACE 指令踪迹 MTRACE 内存访问踪迹 FTRACE 函数调用踪迹 ETRACE 异常/中断踪迹 IRINGBUF 最近指令环形缓冲 DIFFTEST 与参考模拟器对拍 SDB 交互式状态观察本文会逐项对应到代码。
关键词
NEMU、ITRACE、MTRACE、FTRACE、ETRACE、IRINGBUF、DIFFTEST、SDB、riscv32、ysyx
一、踪迹的本质
踪迹不是简单“多打印一点日志”。
它的本质是:
记录状态机中的关键转移例如:
执行了哪条指令 读写哪块内存 调用了哪个函数 发生了什么异常 最近 16 条指令是什么 当前 CPU 状态和参考模拟器是否一致这些信息结合起来,就能回答三类问题:
- 程序从哪里跑到了哪里?
- 状态为什么变成现在这样?
- 出错前最后发生了什么?
二、NEMU 中与踪迹相关的模块
对应代码:
nemu/src/cpu/cpu-exec.c nemu/src/utils/log.c nemu/src/utils/disasm.c nemu/src/utils/ftrace.c nemu/src/memory/paddr.c nemu/src/memory/vaddr.c nemu/src/isa/riscv32/system/intr.c nemu/src/cpu/difftest/ nemu/src/monitor/sdb/对应配置:
nemu/Kconfig nemu/.config典型配置项:
CONFIG_TRACE CONFIG_ITRACE CONFIG_ITRACE_COND CONFIG_MTRACE CONFIG_FTRACE CONFIG_IRINGTRACE CONFIG_ETRACE CONFIG_DIFFTEST三、日志系统:所有踪迹的基础
文件:
nemu/src/utils/log.c代码:
void init_log(const char *log_file) { log_fp = stdout; if (log_file != NULL) { FILE *fp = fopen(log_file, "w"); Assert(fp, "Can not open '%s'", log_file); log_fp = fp; } Log("Log is written to %s", log_file ? log_file : "stdout"); } bool log_enable() { return MUXDEF(CONFIG_TRACE, (g_nr_guest_inst >= CONFIG_TRACE_START) && (g_nr_guest_inst <= CONFIG_TRACE_END), false); }调用方式:
./riscv32-nemu-interpreter -l trace.log image.bin这意味着:
-l 指定日志文件 CONFIG_TRACE 决定是否启用日志 CONFIG_TRACE_START / CONFIG_TRACE_END 决定 trace 区间日志宏:
#define _Log(...) do { \ printf(__VA_ARGS__); \ log_write(__VA_ARGS__); \ } while (0)所以:
Log(...) 既打印到终端,也写入日志文件 log_write(...) 只写日志文件四、ITRACE:指令踪迹
1. 什么时候生成
文件:
nemu/src/cpu/cpu-exec.c代码:
static void exec_once(Decode *s, vaddr_t pc) { s->pc = pc; s->snpc = pc; isa_exec_once(s); cpu.pc = s->dnpc; #ifdef CONFIG_ITRACE char *p = s->logbuf; p += snprintf(p, sizeof(s->logbuf), "%08x:", s->pc); int ilen = s->snpc - s->pc; int i; uint8_t *inst = (uint8_t *)&s->isa.inst; for (i = ilen - 1; i >= 0; i --) p += snprintf(p, 4, " %02x", inst[i]); disassemble(...); #endif }ITRACE记录:
指令地址 指令机器码 反汇编结果例如:
80000000: 00000297 auipc t0,0 80000004: 00028823 sb zero,16(t0)2. 在哪里输出
static void trace_and_difftest(Decode *_this, vaddr_t dnpc) { #ifdef CONFIG_ITRACE_COND if (ITRACE_COND) { log_write("%s\n", _this->logbuf); } #endif if (g_print_step) { #ifdef CONFIG_ITRACE puts(_this->logbuf); #endif } IFDEF(CONFIG_DIFFTEST, difftest_step(_this->pc, dnpc)); }含义:
ITRACE_COND 为真 -> 写入日志文件 g_print_step 为真 -> 打印到终端g_print_step应该由 SDB 控制:
g_print_step = (n < MAX_INST_TO_PRINT);这样:
si 1、si 10 -> 打印 c、-b -> 不逐条打印3. 反汇编
文件:
nemu/src/utils/disasm.c它使用 Capstone:
void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);没有反汇编时,ITRACE 只剩机器码,调试难度会明显增加。
五、IRINGBUF:最近指令环形缓冲
文件:
nemu/src/cpu/cpu-exec.c定义:
#define IRING_BUF_SIZE 16 static char iringbuf[IRING_BUF_SIZE][128 + 4]; static size_t iringbuf_index = 0; static bool iringbuf_initialized = false;写入:
if (iringbuf_initialized) { snprintf(iringbuf[iringbuf_index], sizeof(iringbuf[0]), "--> %s", s->logbuf); iringbuf_index = (iringbuf_index + 1) % IRING_BUF_SIZE; }打印:
static void print_iringbuf() { printf("Recent instructions (most recent last):\n"); ... }IRINGBUF 的作用是:
正常运行时只保留最近 16 条指令 出问题时一次性打印出来这比把全部 ITRACE 写到磁盘更适合定位崩溃前现场。
六、MTRACE:内存访问踪迹
文件:
nemu/src/memory/paddr.c读取:
word_t paddr_read(paddr_t addr, int len) { if (likely(in_pmem(addr))) { word_t ret = pmem_read(addr, len); #ifdef CONFIG_MTRACE if (!is_instruction_fetch) { printf("[MTRACE] Read %2d bytes from PMEM ..."); } #endif return ret; } IFDEF(CONFIG_DEVICE, { word_t ret = mmio_read(addr, len); #ifdef CONFIG_MTRACE printf("[MTRACE] Read %2d bytes from MMIO ..."); #endif return ret; }); }写入:
void paddr_write(paddr_t addr, int len, word_t data) { #ifdef CONFIG_MTRACE if (in_pmem(addr)) { printf("[MTRACE] Write ... PMEM ..."); } else { printf("[MTRACE] Write ... MMIO ..."); } #endif ... }为什么要区分指令取指?
在:
nemu/src/memory/vaddr.c中:
word_t vaddr_ifetch(vaddr_t addr, int len) { is_instruction_fetch = true; word_t ret = paddr_read(addr, len); is_instruction_fetch = false; return ret; }is_instruction_fetch用来避免 MTRACE 把每条指令取指都打印出来,否则日志量会爆炸。
七、FTRACE:函数调用踪迹
文件:
nemu/src/utils/ftrace.cFTRACE 的基本原理:
读取 ELF 符号表 建立 函数地址 -> 函数名 的映射 拦截 jal / jalr / ret 打印函数调用与返回典型输出:
0x80000000: call [main] 0x80000020: call [foo] 0x80000040: call [bar] 0x80000060: ret [bar]FTRACE 依赖 ELF 文件。
在你的代码中:
#ifdef CONFIG_FTRACE char elf_path[256]; strncpy(elf_path, img_file, sizeof(elf_path)-1); char *dot_bin = strstr(elf_path, ".bin"); if(dot_bin != NULL) { *dot_bin = '\0'; } strncat(elf_path, ".elf", ...); parse_elf(elf_path); #endif它会把:
hello.bin转换成:
hello.elf然后解析符号。
使用前提:
编译时开启 CONFIG_FTRACE 镜像旁边存在同名 .elf ELF 中包含符号表八、ETRACE:异常和中断踪迹
文件:
nemu/src/isa/riscv32/system/intr.c异常入口:
word_t isa_raise_intr(word_t NO, vaddr_t epc) { csr_write(MEPC_ADDR, epc); csr_write(MCAUSE_ADDR, NO); if (NO == 3) { nemu_state.state = NEMU_END; nemu_state.halt_pc = epc; nemu_state.halt_ret = 0; return epc; } word_t mtvec = csr_read(MTVEC_ADDR); return mtvec & ~0x3; }ETRACE 的理想输出:
Exception [11] triggered at PC = 0x80000100, entry = 0x80000080 Interrupt [7] triggered at PC = 0x80000200, entry = 0x80000080你的代码中已经预留了类似逻辑:
#ifdef CONFIG_ETRACE bool is_irq = (NO & 0x80000000u) != 0; ... #endif如果被注释掉,需要恢复或重新启用。
ETRACE 适合排查:
ecall后的异常跳转;mret返回错误;mcause/mepc/mtvec设置错误;- 中断没有触发。
九、DIFFTEST:差分测试
文件:
nemu/src/cpu/difftest/ nemu/src/isa/riscv32/difftest/dut.c在trace_and_difftest()中:
IFDEF(CONFIG_DIFFTEST, difftest_step(_this->pc, dnpc));DIFFTEST 的思想是:
NEMU 执行一条指令 参考模拟器也执行一条指令 双方比较 PC、通用寄存器、部分 CSR 如果不一致,立即报错典型错误信息:
DIFFTEST ERROR: Register ... mismatch NPC=..., REF=... at PC=...DIFFTEST 适合排查:
- 指令语义错误;
- PC 更新错误;
- 寄存器写回错误;
- 分支/跳转偏移计算错误;
- CSR 状态不同步。
十、SDB 与踪迹的配合
踪迹负责“自动记录”,SDB 负责“交互观察”。
SDB 提供:
si 单步执行 info r 查看寄存器 x 查看内存 p 表达式求值 w 监视表达式变化 b 设置断点例如:
p $pc x 10 0x80000000 w $a0两者结合:
ITRACE 告诉你“执行了哪些指令” SDB 告诉你“当前状态是什么” IRINGBUF 告诉你“崩溃前最后几条指令” MTRACE 告诉你“谁改了内存” FTRACE 告诉你“调用栈是什么” ETRACE 告诉你“异常怎么触发” DIFFTEST 告诉你“第一条不一致的指令”十一、按问题选择踪迹
| 问题 | 推荐踪迹 |
|---|---|
| 不知道程序执行到哪里 | ITRACE / IRINGBUF |
| 内存被改坏 | MTRACE |
| 函数调用关系错误 | FTRACE |
| 异常/中断错误 | ETRACE |
| 指令实现和参考模拟器不一致 | DIFFTEST |
| 想交互式观察状态 | SDBsi/info r/x/p |
| 想监视变量变化 | SDB watchpoint |
| 崩溃前最后指令 | IRINGBUF |
十二、典型调试流程
1. 打开配置
在 NEMU 目录:
make menuconfig选择:
Testing and Debugging -> Enable tracer -> Enable instruction tracer -> Enable memory access tracer -> Enable ftrace -> Enable iringtrace -> Enable exception tracer -> Enable differential testing重新编译:
make clean make2. 运行并生成日志
./build/riscv32-nemu-interpreter \ -l trace.log \ -b image.bin3. 查看日志
less trace.log重点搜索:
ERROR mismatch exception panic invalid4. 如果崩溃
检查:
IRINGBUF PC 寄存器 异常号 最后访问的内存十三、常见问题
1. ITRACE 不输出
检查:
CONFIG_TRACE CONFIG_ITRACE CONFIG_ITRACE_COND g_print_step如果g_print_step被强制设为true,会导致所有c和-b都逐条打印,性能极差。
正确:
g_print_step = (n < MAX_INST_TO_PRINT);2. MTRACE 太多日志
MTRACE 默认会打印所有内存访问,日志量很大。
建议:
- 只在调试时开启;
- 使用 trace start/end 限制范围;
- 通过
is_instruction_fetch过滤取指。
3. FTRACE 没有函数名
检查 ELF:
riscv64-linux-gnu-readelf -s image.elf | head如果 ELF 没有符号表,FTRACE 无法输出函数名。
4. ETRACE 没有输出
检查:
CONFIG_ETRACE intr.c 中对应代码是否被注释 mcause/mepc/mtvec 是否正确5. DIFFTEST 不可用
检查:
CONFIG_DIFFTEST 参考动态库路径 端口 镜像是否一致 初始寄存器状态是否同步十四、总结
踪迹系统的本质是:
记录状态机的关键事件NEMU 中的踪迹分工:
ITRACE 指令流 MTRACE 内存读写 FTRACE 函数调用 ETRACE 异常中断 IRINGBUF 最近指令 DIFFTEST 与参考实现对比 SDB 交互式观察对应代码:
| 踪迹 | 代码位置 |
|---|---|
| ITRACE | cpu-exec.c、disasm.c |
| IRINGBUF | cpu-exec.c |
| MTRACE | paddr.c、vaddr.c |
| FTRACE | utils/ftrace.c |
| ETRACE | isa/riscv32/system/intr.c |
| DIFFTEST | cpu/difftest/ |
| SDB | monitor/sdb/ |
最终:
不要盲目 printf 先用 ITRACE 看执行流 再用 MTRACE 看内存 用 FTRACE 看调用关系 用 ETRACE 看异常 用 IRINGBUF 看崩溃前现场 用 DIFFTEST 找第一条不一致 用 SDB 观察当前状态这就是 NEMU 中“踪迹”作为 bug 诊断利器的完整使用方法。