x64dbg trace file format specification

x64dbg trace file is a binary file that contains all information about program execution. Each trace file is composed of 3 parts: Magic word, JSON header and binary trace blocks. x64dbg trace file is little-endian.

Magic word

Every trace file will begin with 4 bytes, “TRAC” (encoded in ASCII).

Binary trace blocks

Binary trace data is immediately after header without any padding and might not be aligned to 4-byte boundary. It is defined as a sequence of blocks. Currently, only block type 0 is defined.

Every block is started with a 1-byte type number. This type number must be 0, which means it is a block that describes an instruction traced.

If the type number is 0, then the block will contain the following data:

struct {
    uint8_t BlockType; //BlockType is 0, indicating it describes an instruction execution.
    uint8_t RegisterChanges;
    uint8_t MemoryAccesses;
    uint8_t BlockFlagsAndOpcodeSize; //Bitfield

    DWORD ThreadId;
    uint8_t Opcode[];

    uint8_t RegisterChangePosition[];
    duint RegisterChangeNewData[];

    uint8_t MemoryAccessFlags[];
    duint MemoryAccessAddress[];
    duint MemoryAccessOldData[];
    duint MemoryAccessNewData[];

RegisterChanges is a unsigned byte that counts the number of elements in the array RegisterChangePosition and RegisterChangeNewData.

MemoryAccesses is a unsigned byte that counts the number of elements in the array MemoryAccessFlags.

BlockFlagsAndOpcodeSize is a bitfield. The most significant bit is ThreadId bit. When this bit is set, ThreadId field is available and indicates the thread id which executed the instruction. When this bit is clear, the thread id that executed the instruction is the same as last instruction, so it is not stored in file. The least 4 significant bits specify the length of Opcode field, in number of bytes. Other bits are reserved and set to 0. Opcode field contains the opcode of current instruction.

RegisterChangePosition is an array of unsigned bytes. Each element indicates a pointer-sized integer in struct REGDUMP that is updated after execution of current instruction, as an offset to previous location. The absolute index is computed by adding the absolute index of previous element +1 (or 0 if it is first element) with this relative index. RegisterChangeNewData is an array of pointer-sized integers that contains the new value of register. REGDUMP structure is given below.

typedef struct
    REGISTERCONTEXT regcontext;
    FLAGS flags;
    X87FPUREGISTER x87FPURegisters[8];
    unsigned long long mmx[8];
    MXCSRFIELDS MxCsrFields;
    X87STATUSWORDFIELDS x87StatusWordFields;
    X87CONTROLWORDFIELDS x87ControlWordFields;
    LASTERROR lastError;
    //LASTSTATUS lastStatus; //This field is not supported and not included in trace file.

For example, ccx is the second member of regcontext. On x64 architecture, it is at byte offset 8 and on x86 architecture it is at byte offset 4. On both architecture, it is at index 1 and cax is at index 0. Therefore, when RegisterChangePosition[0] = 0, RegisterChangeNewData[0] contains the new value of cax. If RegisterChangePosition[1] = 0, RegisterChangeNewData[1] contains the new value of ccx, since the absolute index is computed by 0+0+1=1. The use of relative indexing helps achieve better data compression if a lossless compression is then applied to trace file, and also allow future expansion of REGDUMP structure without increasing size of RegisterChanges and RegisterChangePosition beyond a byte.

x64dbg will save all registers at the start of trace, and every 512 instructions(this number might be changed in future versions to have different tradeoff between speed and space). A block with all registers saved will have RegisterChanges=172 on 64-bit platform and 216 on 32-bit platform. This allows x64dbg trace file to be randomly accessed. x64dbg might be unable to open a trace file that has a sequence of instruction longer than an implementation-defined limit without all registers saved.

MemoryAccessFlags is an array of bytes that indicates properties of memory access. Currently, only bit 0 is defined and all other bits are reserved and set to 0. When bit 0 is set, it indicates the memory is not changed(This could mean it is read, or it is overwritten with identical value), so MemoryAccessNewData will not have an entry for this memory access.

MemoryAccessAddress is an array of pointers that indicates the address of memory access.

MemoryAccessOldData is an array of pointer-sized integers that stores the old content of memory.

MemoryAccessNewData is an array of pointer-sized integers that stores the new content of memory.