PA1 基础设施(2)深度解析:NEMU 中 ITRACE、MTRACE、FTRACE、IRINGBUF、ETRACE、DIFFTEST 全解析

本文结合 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 状态和参考模拟器是否一致

这些信息结合起来,就能回答三类问题:

  1. 程序从哪里跑到了哪里?
  2. 状态为什么变成现在这样?
  3. 出错前最后发生了什么?

二、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.c

FTRACE 的基本原理:

复制代码
读取 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
想交互式观察状态 SDB si/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
make

2. 运行并生成日志

复制代码
./build/riscv32-nemu-interpreter \
  -l trace.log \
  -b image.bin

3. 查看日志

复制代码
less trace.log

重点搜索:

复制代码
ERROR
mismatch
exception
panic
invalid

4. 如果崩溃

检查:

复制代码
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 诊断利器的完整使用方法。

相关推荐
YOLO数据集集合2 小时前
EvoAgent:面向PR研发治理的自进化Multi-Agent Harness系统
java·开发语言·目标检测·agent·自进化
知行EDI2 小时前
知行之桥 MaBang 端口使用指南——Create Order 订单创建篇
java·服务器·数据库
笑鸿的学习笔记2 小时前
C++笔记之大块顺序写
java·c++·笔记
成旭先生2 小时前
【2026】企业信息模糊查询 API 实战:名称、注册号、统一社会信用代码、企业类型与法人一次查全
服务器·数据库·数据服务
打工仔折腾 AI2 小时前
从 Demo 到生产级 Agent:8 个关键设计机制与 Python 实现拆解
java·jvm·人工智能·后端·python·langchain·ai agent 实战
殷色玫瑰3 小时前
C/C++ 内存管理详解:从内存分布到 new/delete 底层原理
java·linux·c语言·c++
山岚的运维笔记4 小时前
ComfyUI NVIDIA安装教程:官方便携版下载+run_nvidia_gpu.bat启动,8G显存Windows实操
运维·服务器·windows·笔记·prompt·aigc·comfyui
꯭自꯭闭꯭4 小时前
DM7主备升级方案
linux·服务器·数据库
青山木4 小时前
Hot 100 --- 编辑距离
java·数据结构·算法·leetcode·动态规划