跳转到内容

Trace 格式规范

二进制 Trace 格式是任何采集引擎与 Tenet 之间的契约。只要工具生成合规的 .bin 文件,Tenet 即可分析——QBDITrace 是官方实现,但不是唯一选择。

权威 C 定义位于 tools/qbditrace/TraceFormat.h。本文档为自定义采集器的实现者重新陈述该格式。

536 字节,在文件起始处写入一次。所有多字节整数均为小端序(AArch64 原生)。

偏移 大小 字段 说明
0x000 4 magic 0x51424454(“QBDT”)
0x004 4 version 4(基线)、5(PC-delta)、6(old-value)、7(FPR)
0x008 4 arch 架构标签:1 = AArch64, 2 = x86_64
0x00C 4 num_gpr 锚点快照中的 GPR 数(AArch64: 34)
0x010 8 module_base 被追踪模块的运行时基地址
0x018 8 module_slide ASLR slide(运行时 − 静态首选基址)
0x020 8 trace_start 追踪范围运行时起始(含)
0x028 8 trace_end 追踪范围运行时终止(不含)
0x030 8 flags 头部标志(见下文)
0x038 256 module_name NUL 终止的模块路径
0x138 4 target_os 0=未知, 1=iOS, 2=Android, 3=Linux
0x13C 220 reserved 填充至 536 字节
static_address = runtime_address − module_slide

Tenet 将所有运行时 PC 归一化回静态镜像,以便与 IDA/Ghidra 匹配。

偏移 0x030 的 flags 字段控制记录布局与压缩。标志与版本标签正交:版本由改变磁盘布局的最高标志决定。

名称 效果
0 HF_HAS_MEM 记录可能包含内存访问
1 HF_HAS_NZCV INST 记录包含 NZCV 标志
2 HF_REG_DIFF INST 记录存储寄存器差值(非全量快照)
3 HF_HAS_CODE 流中包含 REC_CODE 记录
4 HF_PC_DELTA v5:INST 头用 2 字节有符号 PC 差值替代 8 字节绝对 PC
5 HF_OLD_VALUE v6:内存访问记录包含写前值(每条 48 B)
6 HF_HAS_FPR v7:ANCHOR 包含 512 B FPR 快照;INST 包含 FPR 差值
7 HF_COMPRESSED 遗留格式:整个记录流为一个 zstd 帧(历史)
8 HF_BLOCK_COMPRESSED 1 MiB zstd 分块 + 尾部索引,支持随机访问(默认)
9 HF_BRANCH_HINT nzcv 高 4 位编码 CALL/RET/BRANCH 分类
版本 门控标志 关键变化
v4 基线 INST 头 16 B, ANCHOR 288 B, 引入 REC_CODE
v5 HF_PC_DELTA INST 头缩至 6 B(int16 PC 差值);解码器维护运行 PC
v6 HF_OLD_VALUE 内存访问记录增至 48 B(写前 old_value 字段);writer 已移除
v7 HF_HAS_FPR ANCHOR 增加 512 B FPR 块(共 800 B);INST 增加 FPR 差值段

不认识某个标志的读取器必须拒绝该文件。

每条记录以单字节 kind 标签开头:

Kind 说明
REC_INST 0 逐指令寄存器/内存差值
REC_ANCHOR 1 全量寄存器快照(重建检查点)
REC_THREAD 2 线程上下文标记
REC_EVENT 3 VM 事件(序列入口/出口、跨区间转移)
REC_SVC 4 系统调用位置
REC_OBJC 5 ObjC msgSend 系列派发
REC_CODE 6 原始指令字节(每 PC 仅一次)
REC_CAPICALL 7 C/平台 API 调用,含语义参数捕获

最频繁的记录。只存储指令产生的变化

v4 布局HF_PC_DELTA 未设置):

qbditrace_inst_head_t 16 字节
kind u8
nzcv u8 (低 4 位 = N/Z/C/V;高 4 位 = branch hint,当 HF_BRANCH_HINT 时)
n_regdiff u8
n_mem u8
pc u64 (运行时绝对 PC)
n_regdiff × regdiff_t 每条 16 字节
reg_index u8 (QBDI GPR 排序:0=x0 … 30=lr, 31=sp, 32=nzcv, 33=pc)
[padding] 7 B
value u64 (指令执行后的值)
n_mem × memacc_t 每条 32 字节
mem_type u8 (bit0 = 读, bit1 = 写)
mem_flags u8 (bit0 = unknown_size, bit1 = minimum_size, bit2 = unknown_value)
size u16
addr u64
value u64 (低 8 字节)
value_hi u64 (9–16 字节 store 的高字节;否则为 0)

v5 布局HF_PC_DELTA 设置):除 pc 替换为 pc_delta(int16 LE)外完全相同。解码器维护 running_pc += pc_delta。若 |delta| > 32767,先发射 ANCHOR(用其绝对 pc 重置 running_pc),然后 delta = 0。头大小:6 字节。

v7 布局HF_HAS_FPR 设置):在 GPR 差值之后、内存访问之前,插入 FPR 差值段:

inst_head(v7 或 v7+v5 变体)
n_regdiff × regdiff_t
n_fprdiff × fprdiff_t 每条 24 字节
fpr_index u8 (0..31 = v0..v31)
[padding] 7 B
lo u64 (bits 63:0)
hi u64 (bits 127:64)
n_mem × memacc_t

周期性全量寄存器检查点。解码器使用最近的锚点重建任意指令的状态。

v4/v5/v6 布局:

anchor_head_t 16 字节
kind u8
[reserved] 3 B
pc u64 (绝对运行时 PC)
num_gpr × u64 272 字节(AArch64: 34 × 8 B)

总计:288 字节。

v7 布局HF_HAS_FPR 设置):GPR 块之后追加 512 字节 FPR 快照:

anchor_head_t + GPR block 288 字节
fpr_lo[32] 256 字节 (v0..v31 的 bits 63:0)
fpr_hi[32] 256 字节 (v0..v31 的 bits 127:64)

总计:800 字节。

thread_head_t 16 字节
kind u8
[reserved] 3 B
thread_id u64

在每个线程流起始处发射。当前 Tenet 假设单线程 trace。

event_head_t 24 字节
kind u8
event u8 (1=SEQ_ENTRY, 2=SEQ_EXIT, 3=XFER_CALL, 4=XFER_RETURN)
[reserved] 2 B
addr u64 (XFER_CALL: 目标地址; XFER_RETURN: 返回目标)
site u64 (XFER_CALL: 调用点 PC; 否则为 0)
svc_head_t 72 字节
kind u8
[reserved] 3 B
pc u64 (svc 指令地址)
syscall_no u64 (Darwin: x16; Linux/Android: x8)
args[6] 6×u64 (x0..x5)
objc_head_t 80 字节
kind u8
variant u8 (0=msgSend, 1=msgSendSuper, 2=msgSendSuper2, 3=msgSend_stret)
[reserved] 2 B
pc u64 (调用点 PC)
recv u64 (x0: 接收者或 super 结构体指针)
sel u64 (x1: 选择子; *Super 变体用 x2)
cls[48] (NUL 终止的运行时类名; 不可用时为 "")

每 PC 一条原始指令字节。首次执行时发射,按 PC 去重。支持无外部镜像的离线反汇编。

code_head_t 16 字节
kind u8
size u8 (指令长度; AArch64 为 4)
[reserved] 2 B
pc u64 (运行时地址)
bytes[size] (原始小端编码)

捕获已知 C/平台 API 函数的调用。固定 88 字节头 + 0–4 个变长内联字符串捕获。

capicall_head_t 88 字节
kind u8
api_id u8 (capi_id 枚举的数值 ID)
arg_count u8 (args[] 有效项数, 0..8)
str_count u8 (后随的内联字符串捕获数, 0..4)
pc u64 (调用点 PC)
target u64 (被调用函数的运行时地址)
args[8] 8×u64 (调用时 x0..x7)

后随 str_count 个变长捕获:

capicall_str_t 4 字节
str_arg_idx u8 (该字符串来自哪个参数, 0..7)
str_len u8 (内容字节数,不含 NUL; 最大 255)
str_flags u8 (bit0=截断, bit1=二进制, bit2=safe-read 失败)
[_reserved] u8
str_data[str_len + 1] (NUL 终止; 填充至 4 字节对齐)

所有新 trace 的默认方式。536 字节头部之后的数据被分为独立的 1 MiB zstd 帧。文件末尾的尾部索引支持 O(1) 随机访问。

文件布局:

[文件头: 536 B, 未压缩]
[block 0: zstd 帧]
[block 1: zstd 帧]
...
[block N: zstd 帧]
[块索引表: N × 24 B 条目]
[块尾部: 16 B]

块索引条目(每条 24 字节):

字段 大小 说明
original_offset u64 逻辑(解压后)字节偏移(从记录起始)
compressed_offset u64 压缩块的物理文件偏移
original_size u32 解压后字节数
compressed_size u32 磁盘上压缩字节数

块尾部(文件最后 16 字节):

字段 大小 说明
index_offset u64 第一个索引条目的文件偏移
block_count u32 压缩块数量
magic u32 0x51424249(“QBBI”)

历史格式:头部之后的整个记录流为单个 zstd 帧。无随机访问。HF_COMPRESSEDHF_BLOCK_COMPRESSED 互斥。

HF_BRANCH_HINT 设置时,REC_INSTnzcv 字节的高 4 位编码指令的分支类别:

高 nibble 含义
0b0000 0 普通指令(非分支)
0b0001 1 CALL(BL / BLR)
0b0010 2 RET(RET / RETAA / RETAB)
0b0100 4 BRANCH(B / BR — 非调用无条件跳转)

此编码零额外字节,让解码器对 ~95–98% 指令跳过 pc_kind 查表。向后兼容:旧 trace 永远不会设置高 nibble,旧读取器不受影响。

GPR 索引(用于 regdiff_t.reg_index 和锚点快照):

索引 寄存器
0–28 x0–x28
29 fp (x29)
30 lr (x30)
31 sp
32 nzcv
33 pc

FPR 索引(用于 fprdiff_t.fpr_index):

索引 寄存器
0–31 v0–v31(128 位 NEON)

Tenet 可打开任何 QBDT magic 且版本 ≥ 4 的 trace。缺失记录优雅降级——仅运行输入满足的 Pass。

要求:合法 QBDT magic、版本 ≥ 4、含 PC 的 REC_INST 记录、至少一个 REC_ANCHOR

结果:基础指令列表与地址归一化。

增加:HF_REG_DIFFHF_HAS_NZCVHF_HAS_MEMHF_HAS_CODEHF_BRANCH_HINTREC_EVENT

结果:完整 CFG 恢复、函数边界检测、调用图、循环分析、寄存器重建、内存模型、污点分析。

增加:REC_SVCREC_OBJCREC_CAPICALL

结果:系统调用命名、ObjC 消息解析、C API 参数捕获与字符串检查。

增加:HF_HAS_FPR(v7)。

结果:NEON/向量寄存器重建、密码学算法识别。

QBDITrace 的常见替代包括 Frida Stalker(通过 Stalker.follow 发射 REC_INST + REC_ANCHOR + REC_CODE)、DynamoRIO、Pin 或 Unicorn 等自定义模拟器——关键在于遵守上文描述的二进制格式。

  1. 写入 536 字节文件头,magic = 0x51424454version = 4、设置相应 flags
  2. 周期性发射 REC_ANCHOR 记录(每 1024 条指令为推荐默认)。每个锚点必须包含完整 GPR 快照。
  3. 对每条执行的指令,发射 REC_INST 记录,含 PC(绝对或差值)、NZCV、寄存器差值和内存访问。
  4. 可选为每个首次执行的 PC 发射 REC_CODE 记录。
  5. 使用分块压缩以支持高效随机访问:按 1 MiB 压缩并追加索引表与尾部。
  6. 正确设置 module_basemodule_slide,使 Tenet 能将运行时地址归一化回静态镜像。
// 写入头部
qbditrace_file_header_t hdr = {0};
hdr.magic = 0x51424454;
hdr.version = 4;
hdr.arch = 1; // AArch64
hdr.num_gpr = 34;
hdr.module_base = runtime_base;
hdr.module_slide = slide;
hdr.trace_start = range_start;
hdr.trace_end = range_end;
hdr.flags = QBDITRACE_HF_REG_DIFF | QBDITRACE_HF_HAS_NZCV
| QBDITRACE_HF_HAS_MEM | QBDITRACE_HF_HAS_CODE
| QBDITRACE_HF_BLOCK_COMPRESSED | QBDITRACE_HF_BRANCH_HINT;
strncpy(hdr.module_name, path, 255);
write(fd, &hdr, 536);
// 每条指令:
qbditrace_inst_head_t inst = {0};
inst.kind = 0;
inst.nzcv = current_nzcv;
inst.n_regdiff = count_written_regs;
inst.n_mem = count_mem_accesses;
inst.pc = runtime_pc;
write(fd, &inst, 16);
write(fd, reg_diffs, inst.n_regdiff * 16);
write(fd, mem_accesses, inst.n_mem * 32);
// 周期性:
qbditrace_anchor_head_t anchor = {0};
anchor.kind = 1;
anchor.pc = runtime_pc;
write(fd, &anchor, 16);
write(fd, gpr_snapshot, num_gpr * 8);

QBDITrace dylib 暴露稳定的 C ABI 用于编程式捕获:

int qbditrace_configure(const qbditrace_config_t *cfg);
int qbditrace_run_call(uint64_t *ret_out, uint64_t fn,
uint32_t argc, const uint64_t *argv);
int qbditrace_run_at(uint64_t *ret_out, uint64_t start_pc,
const uint64_t *gpr31, uint64_t sp, uint64_t lr);
void qbditrace_stop(void);
int qbditrace_force_anchor(void);
const char *qbditrace_output_path(void);
int qbditrace_dump_pcring(const char *path);
const qbditrace_stats_t *qbditrace_stats_page(void);

参见捕获 > 参数与配置了解封装这些调用的 Python loader。