
如何解析 x64dbg trace 文件的二进制格式以读取指令执行记录【免费下载链接】x64dbgAn open-source user mode debugger for Windows. Optimized for reverse engineering and malware analysis.项目地址: https://gitcode.com/gh_mirrors/x6/x64dbg如果你的任务是在 x64dbg 之外读取它的指令执行记录——例如用脚本批量分析 trace、开发自己的回放或统计工具——就需要直接解析 trace 文件的二进制格式。x64dbg 的 trace 文件由 格式规范 定义整个文件是小端序由 Magic word、JSON header 和若干 binary trace block 三部分组成。本文按这个规范说明每个字段的读法、指令块内寄存器与内存访问数据的位置以及如何验证解析结果是否符合预期。trace 文件的整体结构一个 trace 文件从头到尾依次是Magic word文件开头 4 个字节ASCII 的TRAC。JSON header紧跟在偏移 4 处先是一个 4 字节长度字段随后是 JSON blob。注意 JSON blob 可能不以 null 结尾也可能没有对齐到 4 字节边界。Binary trace blocks紧跟在 header 之后没有 padding同样可能不对齐到 4 字节边界。它是一个块的序列每个块以 1 字节类型号开头。也就是说读完 header 后直接按块循环解析即可不需要做任何对齐处理。解析 JSON header先做版本与架构校验x64dbg 官方读取器在 TraceFileReader.cpp 中对 header 做了如下检查写第三方解析器时建议照做遇到不满足的情况直接报错JSON 必须能成功解析否则官方解析器抛出JSON header is corrupted。字段ver必须存在且等于1否则分别抛出Version not found或Version not supported。字段arch必须是x86或x64之一它决定了指针宽度duint为 4 或 8 字节后续所有块解析都依赖它。字段compression存在且值为空字符串。可选字段hashAlgorithm为murmurhash时会有配套的hash字段0x前缀的十六进制字符串path字段保存被调试程序的完整路径。这些字段只用于展示与被调试程序关联不影响指令块解析。逐块解析指令块BlockType 0类型号为0的块描述一条指令的执行布局如下来自 规范struct { uint8_t BlockType; //0表示描述一条指令执行 uint8_t RegisterChanges; uint8_t MemoryAccesses; uint8_t BlockFlagsAndOpcodeSize; //位域 DWORD ThreadId; uint8_t Opcode[]; uint8_t RegisterChangePosition[]; duint RegisterChangeNewData[]; uint8_t MemoryAccessFlags[]; duint MemoryAccessAddress[]; duint MemoryAccessOldData[]; duint MemoryAccessNewData[]; };其中duint是指针大小整数x86 为 4 字节x64 为 8 字节。几个关键字段的读法BlockFlagsAndOpcodeSize位域最高位是 ThreadId 位。该位为 1 时块内存在ThreadId字段4 字节表示执行该指令的线程该位为 0 时块内没有这个字段执行线程与上一条指令相同——所以解析器必须自己记住上一条指令的线程号逐块传递。低 4 位是Opcode字段的字节数其余位保留为 0。Opcode就是当前指令的操作码字节交给反汇编器即可得到助记符。寄存器变化RegisterChanges是RegisterChangePosition和RegisterChangeNewData两个数组的元素个数。RegisterChangePosition的元素是字节表示REGDUMP结构中被更新的成员相对索引绝对索引 上一个元素的绝对索引 1首个元素加 0 本元素的值。RegisterChangeNewData是指针大小整型数组记录这些寄存器在该点的值。相对索引的设计是为了压缩友好并允许REGDUMP以后扩展。指令地址不需要单独字段读cip寄存器即可定位。内存访问MemoryAccesses是MemoryAccessFlags数组的元素个数。MemoryAccessAddress访问地址、MemoryAccessOldData访问前内容各MemoryAccesses个指针大小整型MemoryAccessNewData中某次访问若对应 flag 的 bit 0 为 1表示内存未被改变可能是读也可能是写入了相同值该次访问没有NewData 条目。官方解析器正是按此规则跳块的每次内存访问占 2 或 3 个duint见 readBlock。flag 目前只定义了 bit 0其余位保留为 0要判断访问究竟是读还是写需要配合反汇编器。用户自定义块BlockType ≥ 0x80类型号 ≥0x80的块承载用户自定义数据结构只有三字段struct { uint8_t BlockType; uint32_t BlockSize; uint8_t BlockData[]; };x64dbg 会直接跳过这些块内容可以随意使用。解析器读到这类块时读出 4 字节BlockSize并跳过即可类型号落在1到0x7F之间属于未定义官方解析器会抛出Unsupported block type。完整寄存器快照与随机访问规范说明x64dbg 在 trace 开始时保存全部寄存器之后每 512 条指令再保存一次该数值未来版本可能调整用于速度与空间的权衡。一个全寄存器保存的块在 64 位平台上RegisterChanges 17232 位平台上 216。这正是官方读取器做随机访问的依据TraceFileParser::run 顺序扫描时只在遇到全寄存器块的位置记录页边界fileIndex指令索引 → 文件偏移把文件分成若干页。读取任意一条指令时先按索引找到所在页从该页起点顺序重放块。你的解析器如果只按顺序读文件可以不做页索引如果要支持按索引随机取指令就需要记录这些边界。注意规范中的限制x64dbg 可能无法打开存在超过实现定义长度、且中间没有全寄存器保存的指令序列。REGDUMP结构RegisterChangePosition指向的就是它的成员索引在规范中给出typedef struct { REGISTERCONTEXT regcontext; FLAGS flags; X87FPUREGISTER x87FPURegisters[8]; unsigned long long mmx[8]; MXCSRFIELDS MxCsrFields; X87STATUSWORDFIELDS x87StatusWordFields; X87CONTROLWORDFIELDS x87ControlWordFields; LASTERROR lastError; //LASTSTATUS lastStatus; //该字段不受支持不包含在 trace 文件中 } REGDUMP;REGISTERCONTEXT、FLAGS、X87FPUREGISTER等成员结构的字段定义可以在仓库的 RegisterContext.h 中核对。组合起来读取指令执行记录的解析骨架把以上规则串起来顺序解析的伪代码如下x64 为例指针宽度 8 字节ptr即duintdef parse_trace(f): assert f.read(4) bTRAC # magic word header_len u32(f) # 4 字节长度 header json.loads(f.read(header_len)) assert header[ver] 1 arch header[arch] # x86 或 x64 ptr 4 if arch x86 else 8 last_thread None index 0 while not f.eof(): btype f.read(1)[0] if btype 0x0: # 指令块 reg_changes f.read(1)[0] mem_accesses f.read(1)[0] flags f.read(1)[0] if flags 0x80: last_thread u32(f) # ThreadId 字段存在 else: pass # 沿用 last_thread opcode f.read(flags 0x0F) # 操作码长度取低 4 位 pos f.read(reg_changes) # 相对索引数组 data [uint(ptr, f) for _ in range(reg_changes)] mflags f.read(mem_accesses) addr [uint(ptr, f) for _ in range(mem_accesses)] old [uint(ptr, f) for _ in range(mem_accesses)] new [uint(ptr, f) if (m 1) 0 else None for m in mflags] yield index, last_thread, opcode, (pos, data), (addr, old, new) index 1 elif btype 0x80: # 用户自定义块 size u32(f) f.seek(size, 1) else: raise ValueError(Unsupported block type)三个需要留意的点一是last_thread必须跨块保持因为 ThreadId 只在变化时写入文件二是RegisterChangePosition是相对索引还原绝对成员索引时要按上一绝对索引 1 本值累加首个元素从 0 开始三是每 512 条指令会出现一次全寄存器块RegisterChanges为 172 或 216需要完整还原REGDUMP的基线之后的寄存器变化是相对该基线的增量。验证解析结果与常见错误可以用官方解析器的报错路径作为对照检查见 TraceFileParser::run 的异常分支文件为空File is emptyheader 不是合法 JSONJSON header is corruptedver缺失或不为 1Version not found/Version not supported块类型号不属于 0 或 ≥ 0x80Unsupported block type读取中途 EOF块被截断Read block type failed、Read flags failed、Seek failed等。除此之外符合规范的解析器应满足全文件恰好被 magic、header 和若干块完整消费而无剩余字节统计到的 0 型块数量即 trace 中的指令总数官方读取器据此得到Length()第一条指令块就是 trace 开始时的全寄存器快照172/216。把每条指令的Opcode交给 x64dbg 使用的 Zydis 反汇编器再按cip输出地址就能得到与 x64dbg Trace view 一致的指令执行记录。另外规范指出录制过程中新指令是追加到文件末尾的读取方可以看到文件的实时增长——官方读取器通过丢弃最后一页并重读来刷新。如果你的解析器面对录制尚未结束的文件按读到一个不完整块就停止、稍后重读处理即可。生成一个测试用 trace 文件可选如果手头还没有 trace 文件可以在 x64dbg 命令行中用 StartRunTrace别名StartTraceRecording/opentrace开始录制到指定文件注意默认扩展名trace32/trace64不会自动添加该命令只建立文件并打开 trace 视图实际逐指令记录还需要配合TraceIntoConditional等命令执行程序最后用 StopRunTrace别名StopTraceRecording/tc停止并关闭文件。随后用上面的解析流程读取该文件按验证解析结果一节的条件核对即可。相关文档docs/developers/tracefile.mdtrace 文件格式规范本文主要依据。src/gui/Src/Tracer/TraceFileReader.cpp官方读取器实现header 校验、跳块与页索引逻辑可逐行对照。src/cross/widgets/RegisterContext.hREGISTERCONTEXT等REGDUMP成员结构定义。【免费下载链接】x64dbgAn open-source user mode debugger for Windows. Optimized for reverse engineering and malware analysis.项目地址: https://gitcode.com/gh_mirrors/x6/x64dbg创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考