news 2026/10/3 7:14:14

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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
想交互式观察状态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 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 交互式观察

对应代码:

踪迹代码位置
ITRACEcpu-exec.c、disasm.c
IRINGBUFcpu-exec.c
MTRACEpaddr.c、vaddr.c
FTRACEutils/ftrace.c
ETRACEisa/riscv32/system/intr.c
DIFFTESTcpu/difftest/
SDBmonitor/sdb/

最终:

不要盲目 printf 先用 ITRACE 看执行流 再用 MTRACE 看内存 用 FTRACE 看调用关系 用 ETRACE 看异常 用 IRINGBUF 看崩溃前现场 用 DIFFTEST 找第一条不一致 用 SDB 观察当前状态

这就是 NEMU 中“踪迹”作为 bug 诊断利器的完整使用方法。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 7:14:14

微信小程序WebSocket实时通信:心跳保活与断线重连全攻略

1. 为什么小程序里要单独聊WebSocket这些年做微信小程序开发&#xff0c;最绕不开的一个痛点就是实时通信。聊天界面要刷出新消息、订单状态要立刻变化、游戏对战得同步位置&#xff0c;如果全靠HTTP轮询去定时拉取&#xff0c;性能和体验都撑不住。微信小程序原生的WebSocket能…

作者头像 李华
网站建设 2026/10/3 7:13:55

微信pdf转word怎么弄?手机免下载超简单方法

日常办公、学习中&#xff0c;我们经常会收到PDF文件&#xff0c;不管是合同、论文、工作报表还是学习资料&#xff0c;PDF只能查看不能编辑&#xff0c;修改内容、调整格式都特别麻烦。很多人不知道&#xff0c;其实不用下载任何软件、不用打开电脑&#xff0c;只用微信就能直…

作者头像 李华
网站建设 2026/10/3 7:13:27

Super Agent Party Docker部署完全指南:登录网关与API Key管理轻松搞定

Super Agent Party Docker部署完全指南&#xff1a;登录网关与API Key管理轻松搞定 【免费下载链接】super-agent-party ⭐全能型AI伴侣&#xff01;AI桌面女友 AI虚拟主播 AI即时通讯机器人 AI浏览器 AI智能家居 AI游戏 等你能想到的一切功能&#xff01; 项目地址: ht…

作者头像 李华
网站建设 2026/10/3 7:13:00

免费PDF转Word工具推荐!电脑手机都能用,新手零门槛

日常办公、学习中&#xff0c;我们经常会遇到PDF文件无法编辑、需要修改内容的情况&#xff0c;这时候就需要靠谱的PDF转Word工具。市面上转换工具五花八门&#xff0c;很多要么收费、要么带水印、要么排版错乱&#xff0c;踩坑特别多。今天给大家整理一套真正免费、好用、无套…

作者头像 李华
网站建设 2026/10/3 7:11:51

Simulink表格化电池模型入门:原理、建模步骤与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华