x64dbg 跟踪文件格式规范

x64dbg 跟踪文件是一种二进制文件,包含程序执行的所有信息。每个跟踪文件由三部分组成:魔数JSON 头二进制跟踪块。x64dbg 跟踪文件采用小端序。

魔数

每个跟踪文件以 4 个字节开头,"TRAC"(ASCII 编码)。

头部

头部位于魔数之后,偏移量为 4。它由一个 4 字节的长度字段和一个 JSON 数据块组成。JSON 数据块可能不以空字符结尾,也可能不以 4 字节边界对齐。

二进制跟踪块

二进制跟踪数据紧跟在头部之后,没有任何填充,可能不以 4 字节边界对齐。它被定义为一个块序列。每个块以 1 字节的类型号开始。

如果类型号为 0,则该块包含指令数据:

struct {
    uint8_t BlockType; //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[];
};

如果类型号为 0x80 或更高,则该块包含以下数据:

struct {
    uint8_t BlockType;
    uint32_t BlockSize;
    uint8_t BlockData[];
}

调试器将跳过这些块,您可以将它们用于任何目的。

RegisterChanges 是一个无符号字节,计数数组 RegisterChangePositionRegisterChangeNewData 中的元素数量。

MemoryAccesses 是一个无符号字节,计数数组 MemoryAccessFlags 中的元素数量。

BlockFlagsAndOpcodeSize 是一个位字段。最高有效位是 ThreadId 位。当该位被设置时,ThreadId 字段可用,表示执行该指令的线程 ID。当该位被清除时,执行该指令的线程 ID 与上一条指令相同,因此不存储在文件中。低 4 位指定 Opcode 字段的长度,以字节为单位。其他位保留并设置为 0。Opcode 字段包含当前指令的操作码。

RegisterChangePosition 是一个无符号字节数组。每个元素表示 REGDUMP 结构中在当前指令执行后更新的一个指针大小的整数,作为相对于上一个位置的偏移量。绝对索引通过将上一个元素的绝对索引 +1(如果是第一个元素则为 0)加上这个相对索引来计算。RegisterChangeNewData 是一个指针大小的整数数组,包含寄存器的新值,在指令执行前记录。REGDUMP 结构如下所示。

typedef struct
{
    REGISTERCONTEXT regcontext;
    FLAGS flags;
    X87FPUREGISTER x87FPURegisters[8];
    unsigned long long mmx[8];
    MXCSRFIELDS MxCsrFields;
    X87STATUSWORDFIELDS x87StatusWordFields;
    X87CONTROLWORDFIELDS x87ControlWordFields;
    LASTERROR lastError;
    //LASTSTATUS lastStatus; //此字段不受支持,未包含在跟踪文件中。
} REGDUMP;

例如,ccxregcontext 的第二个成员。在 x64 架构上,它的字节偏移量为 8;在 x86 架构上,它的字节偏移量为 4。在两种架构上,它的索引都是 1,而 cax 的索引为 0。因此,当 RegisterChangePosition[0] = 0 时,RegisterChangeNewData[0] 包含 cax 的新值。如果 RegisterChangePosition[1] = 0,则 RegisterChangeNewData[1] 包含 ccx 的新值,因为绝对索引计算为 0+0+1=1。使用相对索引有助于在对跟踪文件应用无损压缩时实现更好的数据压缩,并且允许未来扩展 REGDUMP 结构而不增加 RegisterChangesRegisterChangePosition 的大小(超过一个字节)。注意:文件读取器可以使用此结构中的 cip 寄存器来定位指令的地址。

x64dbg 会在跟踪开始时保存所有寄存器,并每 512 条指令保存一次(此数字可能在未来版本中更改,以在速度和空间之间进行不同的权衡)。保存所有寄存器的块的 RegisterChanges 在 64 位平台上为 172,在 32 位平台上为 216。这允许随机访问 x64dbg 跟踪文件。如果跟踪文件中存在超过实现定义限制的指令序列且没有保存所有寄存器,x64dbg 可能无法打开该跟踪文件。

MemoryAccessFlags 是一个字节数组,表示内存访问的属性。目前只定义了第 0 位,其他所有位都保留并设置为 0。当第 0 位被设置时,表示内存未更改(这可能意味着它是读取的,或者被相同的值覆盖),因此 MemoryAccessNewData 将不会为此内存访问添加条目。文件读取器可以使用反汇编器来确定内存访问的真实类型。

MemoryAccessAddress 是一个指针数组,表示内存访问的地址。

MemoryAccessOldData 是一个指针大小的整数数组,存储内存的旧内容。

MemoryAccessNewData 是一个指针大小的整数数组,存储内存的新内容。