跳转到内容

恢复程序结构

结构恢复把执行时间轴组织为代码位置、函数、基本块、边、调用和循环。它通常是分析第一步,因为后续 Pass 会复用这些结果。

xref ──> cfg ──> cfg_layout
└──> loop
function ──> call_graph
Pass 依赖 回答的问题 主要结果
function 观察到了哪些 call/return 和函数调用? 调用事件、动态调用栈、函数表
xref 某 PC 在哪里执行、执行几次? PC→inst_id 出现位置与计数
cfg xref 已执行基本块如何连接? 块、边、执行热度、终结指令
cfg_layout cfg CFG 节点如何布局? 可视化坐标、annotated DOT
call_graph function 已观察函数互相调用? caller→callee 边、调用点、次数
loop cfg 哪些 CFG 回边构成循环? header、body、迭代数与嵌套

依赖图由 PassManager 自动解析:请求 --export-cfg 会自动注册 xrefcfgcfg_layout;请求 --call-graph 自动注册 functioncall_graph

function 通过配对 BL/BLR (call) 与 RET 来识别函数边界,使用模拟调用栈。这是动态重建:

  • 只有当本次运行观察到对其入口 PC 的调用时,函数才会出现。
  • 间接调用 (BLR) 和运行时派发会解析到实际观察目标 PC。
  • 尾调用(重用调用者栈帧的无条件 B/BR)通过 SP 跟踪检测,标记为 is_tail_call
  • 每个 CallEvent 记录 call_inst_idtarget_pcreturn_inst_id(未返回时为 0)以及调用栈深度。
  • 函数名来自二进制符号表;无名条目显示为 sub_<hex>

默认情况下内存使用有上限(call events 上限 2000 万,单个函数的 indices 上限 1000)。仅在小 trace 需要完整逐调用导航时使用 FunctionConfig::full_mode

xref 在所有已执行指令上建立 PC→inst_id 索引。它驱动:

  • CFG 面板中的执行计数热力图。
  • 同一块或 PC 各次出现的 首/上/下/末 导航。
  • 前向污点加速(默认启用;用 --taint-no-xref 禁用)。

cfg 从已执行基本块构建控制流图。--export-cfg 将当前 Trace 恢复出的全量 CFG 导出为一个 Graphviz DOT 文件,而不是只导出某个函数:

Terminal window
./tenet [options] trace.bin --export-cfg all.dot

输出是 cfg_layout 的 annotated DOT(CFGLayoutResult::dot_annotated)。用 dot -Tsvg all.dot -o all.svg 或任意 Graphviz 渲染器打开。块颜色反映执行热度;边显示观察到的控制流。

cfg_layout 使用层级网格算法(类似 Ghidra decompiler 布局)为 CFG 节点计算可视化坐标。--export-cfg 会自动注册它;其他布局视图按需请求时再运行。独立 CLI 使用较少;大多数用户通过 Tauri 前端或生成的 DOT 消费布局。

loop 通过 CFG 上的 dominator tree 分析检测自然循环(LLVM 风格 Semi-NCA 算法)。每个循环报告:

  • header_pcback_edge_pc
  • iteration_count(回边被执行的次数)
  • nesting_depth(0 = 最外层)
  • body_pcs(循环体内所有 PC)

循环结果为 Loop Semantics、Trace Fold 和热点分析提供输入。

call_graphfunction 结果聚合为过程间调用图。--call-graph 导出为 Graphviz DOT:

Terminal window
./tenet [options] trace.bin --call-graph cg.dot

每个节点是一个函数(按入口 PC);边携带调用次数和调用点 PC。

  1. 在 Tauri 桌面应用中打开 trace(前端请求后端运行默认 Pass),或直接运行 ./tenet trace.bin 进入 TUI 并启动分析。
  2. 浏览 Functions 面板定位目标函数。
  3. 使用 Call Stack 面板跟随 call/return 导航。
  4. 打开 CFG 面板检查热块和边缘;使用 上/下 访问 在多次执行间步进。
  5. 在 Loops 面板查看迭代次数和嵌套。
  6. 如需持久产物,使用 CLI --export-cfg--call-graph
  • Functions 面板:支持按名称过滤,并按名称、调用次数或指令数排序;单击选择条目,双击在 Hex Dump 中打开函数地址。
  • Call Stack 面板:随当前指令刷新调用栈;单击帧跳转到对应 call site。
  • CFG 面板:显示块、边和执行热度,支持块选择、平移、滚轮/按钮缩放和视图重置。
  • Call Graph 面板:提供可平移缩放的图视图和热点函数表视图;点击节点或表格行会通过 XRef 跳到首次观察到的执行。
  • TUI XRef 导航:首/上/下/末访问导航和 X 快捷键属于 TUI 的键盘交互;不要把它们当作 Tauri 前端快捷键。
  • TUI:pass function:pass xref:pass cfg:pass loop:pass call_graph 运行单个 Pass。:analyze 运行默认集。
  1. functions——列出已观察函数(入口 PC、名称、调用次数、指令数)。
  2. find_function——按 PC 定位函数。
  3. xref——查询 PC→inst_id 出现。
  4. cfg_export——以 DOT 或 JSON 格式导出 CFG(自动运行 xrefcfgcfg_layout)。
  5. call_graph——以 DOT 或 JSON 格式导出调用图(自动运行 functioncall_graph)。
  6. loops——查询检测到的循环(header、body、迭代数、嵌套)。

MCP 工具返回的 PC 使用当前 image_base(默认 0x100000000,与 AArch64 Mach-O 的 IDA 地址一致)。如果静态工具使用不同 base,用 set_image_base 调整。

  • 动态覆盖:仅当本次运行执行过相应证据时,边、函数或循环才会出现。未执行错误路径、其他 switch 分支和 callback 不会出现。
  • 间接调用:解析到观察目标;如果函数指针在运行期间有多个 callee,每条边单独出现。
  • 执行次数:只描述这次输入与运行,不代表一般概率。
  • 函数边界:来自观察到的 call/return 对,不是二进制元数据。即使没有被观察到的调用者,只要函数是 trace 起点仍会出现。
  • Branch hint (HF_BRANCH_HINT):Trace 中存在时,tracer 在录制时将 call/ret/branch 类别编码到 NZCV 高 4 位。function 使用这条 O(1) 路径即时分类 ~95% 的指令。没有 branch hint 时,回退到对 code table 的二进制搜索表(仍正确,略慢)。
  • Sampling:如果 trace 使用指令采样,某些 call/return 对可能丢失,导致不完整的调用栈或幽灵尾调用。FunctionResult::non_local_unwinds_ 统计检测到的异常数。
  • 缺码REC_CODE 禁用):没有嵌入 opcode 时 code table 为空,function 无法分类 branch-class PC。call/return 配对会降级或失败。请用启用 code embedding 重新录制,或提供外部镜像用于反汇编。

调用栈显示异常深度或非局部展开

Section titled “调用栈显示异常深度或非局部展开”

Trace 可能缺失 RET 事件(采样、插桩间隙)或存在非标准控制流(longjmp、异常展开)。检查 FunctionResult::non_local_unwinds_ 获取检测到的异常计数。

该函数在追踪执行期间未被调用,或其调用使用了间接分支且目标未被观察。确认 trace 范围覆盖该代码路径,并在 Xref 中检查预期入口 PC。

确认 trace 包含足够多的已形成基本块的执行指令。非常短的 trace 或重度采样的 trace 可能只产生单块图。同时确认 REC_CODE 或外部镜像可用,以便解析块边界。

循环检测依赖 CFG 准确性。如果采样遗漏了回边,迭代数会被少算。使用未采样的 trace 录制以获得准确循环指标。