- 硬件仿真
- 游戏开发
【免费下载链接】Mesen
Mesen is a cross-platform (Windows & Linux) NES/Famicom emulator built in C++ and C#
Trace Logger(执行日志窗口)是 Mesen 调试器家族中的核心工具之一,用于实时显示并记录 CPU 逐条指令的执行轨迹,可回溯最近 30,000 条指令、按需落盘保存,并支持高度可定制的输出格式与条件过滤。读完本文,你将掌握如何打开与配置 Trace Logger、用格式标签定制每一行日志的输出内容,以及如何借助与断点同源的表达式系统精准筛选你关心的指令(如卡带寄存器写入、PPU 寄存器读取等)。
基本功能与使用场景
Trace Logger 位于 Mesen 调试器(Debugger)中,其核心职责是展示 CPU 的执行日志:每当 CPU 执行一条指令,Trace Logger 就会生成一行对应记录。窗口最多可同时显示最近30,000 条已执行的 CPU 指令(该上限直接对应核心源码中TraceLogger::ExecutionLogSize = 30000常量,见 Core/TraceLogger.h)。
除了屏幕实时查看,你还可以点击Start Logging按钮将指令日志持续写入磁盘文件,生成纯文本日志:
- 点击Start Logging会弹出保存对话框,默认文件名形如
Trace - <ROM名>.txt,并直接写入用户指定的路径(实现见 GUI.NET/Debugger/frmTraceLogger.cs 中的btnStartLogging_Click); - 再次点击(此时按钮变为Stop Logging)即停止记录;
- 停止后可通过Open Trace按钮直接在系统中打开生成的日志文件。
需要特别警惕的是日志文件的膨胀速度:文档明确指出,日志文件可能在几秒内增长到数 GB。因此建议按需开启、记录尽量短的时间段即可,避免磁盘被迅速写满。从源码看,核心层在写入时维护了一个约 32 KB 的内存缓冲(_outputBuffer),只有缓冲超过32768字节时才真正刷入文件(见 Core/TraceLogger.cpp),这在一定程度上缓解了频繁磁盘 IO 的压力,但并不能改变数据本身的巨大体量。
从源码结构看,Trace Logger 采用环形缓冲区设计:_cpuStateCache、_ppuStateCache、_disassemblyCache三个数组各含 30000 个槽位,_currentPos循环递增(AddRow中_currentPos = (_currentPos + 1) % ExecutionLogSize,见 Core/TraceLogger.cpp),因此最旧的记录会被新记录覆盖——这正是"最多显示最近 30000 条"的底层保证。
显示选项:按需开关日志元素
Trace Logger 提供一组复选框,用于控制日志行中显示哪些元素:
- Registers:A / X / Y / SP / P 等 CPU 寄存器
- CPU Cycles:CPU 总周期数(
CycleCount) - PPU Cycles:当前 PPU 周期(0–340)
- PPU Scanline:当前 PPU 扫描线(-1 到 260)
- Show Effective Addresses:显示间接寻址模式计算出的有效地址
- Byte Code:指令对应的机器码字节(1–3 字节)
- Frame Count:PPU 帧计数
- Additional Information (IRQ, NMI, etc.):额外的中断信息标记
这些复选框在 GUI 层由 GUI.NET/Config/DebugInfo.cs 中的TraceLoggerOptions类持久化保存(ShowByteCode、ShowRegisters、ShowCpuCycles、ShowPpuCycles、ShowPpuScanline、ShowPpuFrames、ShowExtraInfo、ShowEffectiveAddresses、ShowMemoryValues等字段),并通过EntityBinder与窗体上的复选框双向绑定(见 GUI.NET/Debugger/frmTraceLogger.cs)。
值得说明的是,这些开关本质上是"格式生成器":当未启用 Format Override 时,GUI 会根据勾选状态动态拼出一条默认格式字符串(见下文"默认格式解析"一节),底层真正渲染日志的仍是格式引擎。因此若你发现勾选项在日志中未生效,请先确认是否意外开启了Format Override(开启后上述复选框会被禁用,见UpdateFormatOptions)。
状态标志格式(Status Flag Format)
CPU 状态寄存器P(Processor Flags)有三种展示风格,由StatusFlagFormat枚举定义(见 GUI.NET/Config/DebugInfo.cs):
| 格式 | 说明 | 对应格式标签 |
|---|---|---|
| Hexadecimal(十六进制) | 以单个十六进制字节直接输出整个 P 寄存器 | P:[P,h] |
| Text(完整文本) | 8 个标志位全部显示,置位用大写字母、未置位用小写字母 | P:[P,8] |
| CompactText(紧凑文本) | 只显示当前置位(为 1)的标志字母 | P:[P] |
标志字母对应关系可在 Core/TraceLogger.cpp 的GetStatusFlag中看到:依次为N V - - D I Z C(活动)与n v - - d i z c(非活动),其中第 3、4 位(对应中断标志 B 与保留位)恒为-。
基于栈指针的缩进
启用Indent code based on stack pointer(按栈指针缩进代码)后,日志行会根据当前栈指针 SP 的深度自动缩进:栈上每压入一个字节,行首多缩进 1 个字符。实现上,缩进量为0xFF - cpuState.SP(见 Core/TraceLogger.cpp)。这一功能对快速识别函数调用与嵌套层级极有帮助——调用越深、缩进越多,调用链结构一目了然。
标签替换(Use Labels)
启用Use Labels后,凡是命中已定义标签(Label)的地址,日志中会直接显示标签名而非十六进制地址。标签机制由LabelManager提供,核心渲染时通过disassemblyInfo.ToString(..., labelManager, ...)传入(见 Core/TraceLogger.cpp)。关于标签的创建与编辑,可参考 调试器文档 的 Labels 一节。
行数上限与自动刷新
窗口右上角的菜单可以切换显示行数(100 / 1000 / 5000 / 10000 / 15000 / 30000 行,见SetLineCount),并通过定时器(1000 行以上每 250ms、以下每 150ms)自动刷新最新日志(见 GUI.NET/Debugger/frmTraceLogger.cs)。由于日志渲染在后台线程完成(Task.Run),模拟器运行中的 UI 不会因此卡顿。
自定义格式:用标签精确控制每一行
勾选Format Override后,Format文本框变为可编辑,你可以完全掌控日志每一行的输出结构。格式字符串由普通文本与方括号标签混合组成,支持以下标签:
[ByteCode]: 指令对应的字节码(1 到 3 字节) [Disassembly]: 当前指令的反汇编文本 [EffectiveAddress]: 间接寻址模式所使用的有效地址 [MemoryValue]: 指令所引用的内存地址中存储的值 [PC]: 程序计数器 [A]: A 寄存器 [X]: X 寄存器 [Y]: Y 寄存器 [SP]: 栈指针 [P]: 处理器标志位 [Cycle]: 当前 PPU 周期 [Scanline]: 当前 PPU 扫描线 [FrameCount]: 当前 PPU 帧号 [CycleCount]: 当前 CPU 周期(64 位无符号值,上电时清零)这些标签与核心层RowDataType枚举一一对应(ByteCode、Disassembly、EffectiveAddress、MemoryValue、PC、A、X、Y、SP、PS、Cycle、Scanline、FrameCount、CycleCount,见 Core/TraceLogger.h),因此你只能在上述范围内选择标签,其他名称会被识别为无效标签并显示[Invalid tag]占位符(见 Core/TraceLogger.cpp)。
标签选项:宽度、进制与对齐
每个标签还可以通过逗号追加选项,语法为[标签,宽度,h]:
[Cycle,3] 将周期值补齐为固定 3 字符宽度输出 [Scanline,h] 以十六进制显示扫描线 [Align,50] 特殊标签:让下一个标签从第 50 列开始输出从实现看,格式字符串在核心层由正则(\[\s*([^[]*?)\s*(,\s*([\d]*)\s*(h){0,1}){0,1}\s*\])|([^[]*)解析(见 Core/TraceLogger.cpp):第二个捕获组是标签名,第四个捕获组是最小宽度MinWidth,第五个捕获组h表示十六进制输出。渲染时WriteValue会先输出数值/文本,再用空格补齐到MinWidth;h修饰符则让数值通过HexUtilities::ToHex转为十六进制(见 Core/TraceLogger.cpp)。
默认格式字符串解析
了解默认格式有助于你快速上手自定义。当未勾选 Override 时,GUI 会按以下逻辑拼装格式(见 GUI.NET/Debugger/frmTraceLogger.cs):
[PC,h] [ByteCode,11h] [Disassembly] → 然后是 [EffectiveAddress]、[MemoryValue,h] [A/寄存器块] CYC:[Cycle,3] SL:[Scanline,3] FC:[FrameCount] CPU Cycle:[CycleCount]其中:
[ByteCode,11h]表示字节码以十六进制显示并占 11 字符宽度;[Align,40](勾选有效地址后对齐列右移至 48,再勾选内存值则右移至 54)用于将右侧的寄存器信息整齐对齐;- 寄存器块按状态标志格式选择
P:[P,h](十六进制)、P:[P,8](完整文本)或P:[P](紧凑文本),并始终输出A:[A,h] X:[X,h] Y:[Y,h] SP:[SP,h]。
一个典型未勾选 Byte Code、但勾选了寄存器的行会形如:
$C000 LDA $2002 A:$00 X:$FF Y:$00 P:[nv--dizc] SP:$FD CYC:12 SL:4(实际列宽与对齐以你的勾选组合为准。)
条件记录:只记录你关心的指令
Condition输入框是整个 Trace Logger 最强大的能力所在:只有满足条件的指令才会被记录。它接受与断点完全相同的表达式语法(详见调试器文档中的断点配置与其中的 Watch 表达式语法一节),支持 C/C++ 风格运算符、$/%前缀的十六进制/二进制字面量、[地址]/{地址}内存读取、以及大量内置变量。
条件表达式可用的内置变量
从 Core/ExpressionEvaluator.cpp 的变量解析代码可以确认,条件中可直接使用以下变量(与断点、监视窗口同源):
| 类别 | 变量 |
|---|---|
| 寄存器 | a、x、y、sp、ps、pc、oppc(当前指令首字节地址)、previousoppc |
| PPU 状态 | cycle(PPU 周期)、scanline(PPU 扫描线)、frame(PPU 帧号) |
| 内存访问 | value(当前读写值)、address(当前 CPU 地址) |
| 标志位 | isread、iswrite、branched、irq、nmi、sprite0hit、spriteoverflow、verticalblank |
官方示例
文档给出三个极具代表性的用法,均可在实际操作中直接复制使用:
# 记录所有对卡带寄存器区($8000 及以上)的写入 IsWrite && Address >= $8000 # 记录所有对 PPU 寄存器($2000-$3FFF)的读取 IsRead && Address >= $2000 && Address <= $3FFF # 只记录 CPU 执行落在 $8100-$8150 地址段内的指令 pc >= $8100 && pc <= $8150第一个例子可用于追踪游戏代码向 mapper 寄存器写入了哪些值(对逆向 ROM 的 bank 切换逻辑极有价值);第二个例子适合观察游戏对 PPU 控制寄存器的访问序列;第三个例子则把日志收窄到某个特定代码区间,配合上文的栈指针缩进即可快速梳理该区间的调用流程。
条件过滤的底层原理
从 Core/TraceLogger.cpp 的ConditionMatches可以看到:SetOptions时条件字符串会先经ExpressionEvaluator::GetRpnList编译为 RPN 逆波兰表达式(_conditionData),每执行一条指令时再用当时的 CPU/PPU 状态与内存操作信息求值;只有结果为真才写入日志。对于不满足条件的指令,_pendingLog机制会保留其状态快照,供该指令在后续子周期(如多周期指令的中间读写)再次判定,确保"整条指令"而非"某个子周期"被一致地过滤。GUI 层还会在条件语法错误时通过警告图标提示"Condition contains invalid syntax or symbols"(见 GUI.NET/Debugger/frmTraceLogger.cs)。
进阶技巧与源码级调用链
记录 IRQ / NMI 等额外信息
当勾选Additional Information且日志正在写入文件时,Trace Logger 会在中断发生时插入标记行。中断标记由 CPU 核心在 NMI/IRQ 触发处发起:CPU.cpp在 Core/CPU.cpp 的 NMI/IRQ 处理中调用DebugAddTrace("NMI")/DebugAddTrace("IRQ"),经由Console::DebugAddTrace(Core/Console.cpp)与Debugger::AddTrace(Core/Debugger.cpp)最终到达TraceLogger::LogExtraInfo,以[NMI - Cycle: <周期数>]的形式写入文件(见 Core/TraceLogger.cpp)。这些标记能帮助你在海量日志中快速定位每次中断发生的精确 CPU 周期。
换行风格与日志可读性
UseWindowsEol选项决定日志使用\r\n(Windows)还是\n(Unix)换行;其默认值在 GUI.NET/Config/DebugInfo.cs 中按运行平台自动选择(!Program.IsMono,即 Windows 上默认\r\n)。此外ExtendZeroPage选项可控制零页地址的显示扩展方式,勾选后反汇编文本会以更完整的形式呈现零页寻址。
从日志行跳转到代码/内存
在 Trace Logger 窗口的日志行上单击右键,可以:
- View in Disassembly:在反汇编窗口中定位到该行的指令;
- Edit in Memory Viewer:在内存查看器中打开该行地址。
右键时若光标落在$XXXX形式的地址上则直接跳转该地址,否则使用当前行的 PC;若命中标签名则跳转到标签(见 GUI.NET/Debugger/frmTraceLogger.cs)。这为"日志中发现可疑地址 → 立即查看上下文"的工作流提供了快捷键级别的效率。
配置的持久化
Trace Logger 的窗口位置、字体(字体族/字号/样式/缩放)、自动刷新开关、显示行数以及全部选项均随调试器配置持久化保存,关闭窗口时写回DebugInfo(见 GUI.NET/Debugger/frmTraceLogger.cs),无需每次重新设置。
GUI → 核心的完整调用链
理解调用链有助于你定位问题或扩展功能。GUI 窗体通过InteropEmu调用互操作层(见 InteropDLL/DebugWrapper.cpp 的DllExport导出函数),再转发到核心TraceLogger:
frmTraceLogger (C#) → InteropEmu.DebugSetTraceOptions / DebugStartTraceLogger / DebugStopTraceLogger → DebugWrapper.cpp DebugSetTraceOptions / DebugStartTraceLogger / ... → Core TraceLogger::SetOptions / StartLogging / StopLogging → GetExecutionTrace 将环形缓冲中的最近 N 行(含 PC、字节码与格式化文本,以 \x1 分隔)返回 GUI 渲染其中GetExecutionTrace会先把三个缓存数组整体拷贝到副本(_xxxCacheCopy),再在锁外逐行格式化,避免渲染过程中被新日志写入破坏一致性(见 Core/TraceLogger.cpp);GUI 端则解析返回的PC \x1 字节码 \x1 行文本三元组,将 PC 作为行号、字节码作为行注释展示(见 GUI.NET/Debugger/frmTraceLogger.cs)。
小结
Mesen 的 Trace Logger 是一套"显示 + 落盘 + 过滤"三层能力兼备的执行日志工具:环形缓冲保证最近 30000 条指令随时可查;Start Logging 支持将任意时长的完整指令流落盘(务必注意文件体积);Format Override 配合 14 种标签与宽度/进制/对齐选项可输出完全自定义的日志行;而条件表达式则让你能从每秒数百万条指令的洪流中精准捞出目标——无论是 mapper 寄存器写入、PPU 寄存器访问,还是某个地址段的代码执行。结合右键跳转反汇编/内存查看器与 IRQ/NMI 标记,它足以支撑从游戏逆向、ROM 汉化到 mapper 行为分析的完整调试工作流。
进一步阅读:断点与表达式语法的完整说明见调试器文档;核心实现位于 Core/TraceLogger.h 与 Core/TraceLogger.cpp;GUI 界面与默认格式逻辑见 GUI.NET/Debugger/frmTraceLogger.cs。
- 硬件仿真
- 游戏开发
【免费下载链接】Mesen
Mesen is a cross-platform (Windows & Linux) NES/Famicom emulator built in C++ and C#
相关推荐
PyTorch Lightning 日志系统完全指南:内置 Logger、自动记录与自定义扩展实战
PyTorch Lightning 日志系统完全指南:内置 Logger、自动记录与自定义扩展实战 PyTorch Lightning 内置了完整、统一的实验日
人工智能深度学习机器学习预训练分布式训练微调Apache APISIX file-logger 插件完全指南:文件日志落盘、自定义格式与按条件过滤
Apache APISIX file logger 插件完全指南:文件日志落盘、自定义格式与按条件过滤 file logger 是 Apache APISIX
后端微服务云原生Faraday 内置 Logger 中间件完全指南:请求/响应日志记录、敏感信息过滤与自定义格式化
Faraday 内置 Logger 中间件完全指南:请求/响应日志记录、敏感信息过滤与自定义格式化 本文围绕 Faraday 内置的 Logger 响应中间件展
后端网络通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考