ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

[机翻] [ABD] 08_数据流分析与优化

2026/8/5 16:09:52 拓冰建站 浏览量
[机翻] [ABD] 08_数据流分析与优化

08 数据流分析与优化

https://github.com/malrev/ABD

本文档讲解数据流分析的两大核心技术——可达定义分析与活跃度分析,阐述它们与编译器优化的深层联系,并通过 Miasm 的三个 Hands-on 实践(死代码移除、死代码保护、真实二进制优化)展示具体应用。

⚠️ API 版本说明:本文档中的 Miasm API 代码示例已于 2026-07 更新至 Miasm 0.1.5+ 版本。课程原始材料使用的是 0.1.3 版本(2020年),部分 API 已发生 breaking change。更新处以 【更新】 标注,旧版用法以 【旧版 v0.1.3】 标注。


目录

  1. 可达定义分析(Reachable Definition Analysis)
  2. 活跃度分析(Liveness Analysis)
  3. 深层洞察:编译器与二进制分析的统一
  4. Hands-on2:死代码移除实践

一、可达定义分析(Reachable Definition Analysis)

1. 定义

可达定义分析(Reaching Definition Analysis)回答以下问题:

当程序执行到某点 p 时,每个变量 x 的当前值是在哪里定义的?

即,从某个定义点 d: x = ... 出发,是否存在一条执行路径到达使用点 p(且路径上 x 未被重新定义),使得定义 d 在 p 处仍然"可达"。

2. 性质

  • 前向数据流分析(Forward dataflow analysis):信息沿控制流方向(从前向后)传播。
  • 从入口基本块开始,沿控制流图逐步传播定义信息。

3. 数据流方程

对于每个基本块 B:

OUT[B] = GEN[B] ∪ (IN[B] - KILL[B])
IN[B]  = ∪ OUT[P]    (P 为 B 的所有前驱)
  • IN[B]:到达 B 入口处的所有定义集合。
  • OUT[B]:到达 B 出口处的所有定义集合。
  • GEN[B]:B 中新产生的定义(B 中对变量的赋值,覆盖之前的定义)。
  • KILL[B]:B 中被覆盖的定义(B 中赋值导致其他位置对同一变量的定义不再可达)。

4. 应用

应用 说明
Constant Propagation / Folding(常量传播/折叠) 若可达定义显示某变量在所有路径上都被赋为常量,则可在使用处直接替换为该常量,并对常量表达式求值折叠
Transform Expressions(变换表达式) 利用定义-使用链对表达式进行等价变换,简化代码

常量传播示例

// 原始代码
x = 5          // 定义 d1: x = 5(常量)
y = x + 3      // 使用 x,可达定义为 d1,x ≡ 5// 常量传播 → y = 5 + 3// 常量折叠 → y = 8

二、活跃度分析(Liveness Analysis)

1. 定义

活跃度分析(Liveness Analysis)回答以下问题:

从程序点 p 开始,沿某条执行路径,p 处变量 x 的值是否可能在后续被使用?

若 x 的值在 p 之后可能被使用,则称 x 在 p 处是活跃的(live);否则称 x 在 p 处是死的(dead)

2. 性质

  • 后向数据流分析(Backward dataflow analysis):信息逆控制流方向(从后向前)传播。
  • 从出口基本块开始,逆控制流图逐步传播活跃信息。

3. 数据流方程

对于每个基本块 B:

IN[B]  = USE[B] ∪ (OUT[B] - DEF[B])
OUT[B] = ∪ IN[S]    (S 为 B 的所有后继)
  • IN[B]:B 入口处活跃的变量集合。
  • OUT[B]:B 出口处活跃的变量集合。
  • USE[B]:B 中在使用前未被定义的变量(即 B 中先使用后定义的变量)。
  • DEF[B]:B 中在使用前被定义的变量(即 B 中先定义后使用或不使用的变量)。

4. 应用

应用 说明
Dead Code Elimination(死代码消除) 若某赋值 x = ... 的左值 x 在该赋值后不再活跃(即 x 的值永不被使用),则该赋值为死代码,可安全删除

死代码消除示例

// 原始代码
x = 5          // 赋值 1
x = 10         // 赋值 2:覆盖赋值 1,且赋值 1 后 x 未被使用
y = x + 1      // 使用 x(活跃定义为赋值 2)// 活跃度分析:赋值 1 的 x 在赋值 2 之前不活跃 → 死代码// 消除后:// x = 10// y = x + 1

三、深层洞察:编译器与二进制分析的统一

1. 数据流分析都是编译器后端 IR 优化技术

可达定义分析与活跃度分析都源自编译器后端的 IR 优化

  • 可达定义分析 → 常量传播、复写传播(Copy Propagation)
  • 活跃度分析 → 死代码消除、寄存器分配

这些技术在编译器中用于优化生成的机器码质量,在二进制分析中用于反混淆。

2. 混淆是优化的反面

维度 编译器优化 混淆 反混淆
死代码 消除死代码 插入死代码(垃圾代码) 消除死代码(活跃度分析)
常量 折叠常量表达式 拆解常量为复杂表达式 传播/折叠常量(可达定义分析)
方向 简化 复杂化 再次简化

3. 编译器与二进制分析工具使用相同的 IR 技术

┌─────────────────────────────────────────────────────┐
│              编译器流水线                             │
│  源代码 → 前端 → IR → {数据流分析 + 优化} → 后端 → 机器码│
│                    ↑                                │
│            可达定义分析 / 活跃度分析                   │
│            (用于常量传播 / 死代码消除)                │
├─────────────────────────────────────────────────────┤
│              二进制分析工具流水线                      │
│  二进制 → 反汇编 → IR → {数据流分析 + 优化} → 反编译    │
│                       ↑                             │
│             可达定义分析 / 活跃度分析                  │
│             (用于常量传播 / 死代码消除 = 反混淆)       │
└─────────────────────────────────────────────────────┘

关键结论:编译器后端的 IR 优化 pass(常量传播、死代码消除等)可以直接复用于二进制反混淆,因为二者操作的 IR 表示与分析算法完全一致。


四、Hands-on2:死代码移除实践

本节通过三个 Miasm Jupyter Notebook 实践,展示数据流分析在反混淆中的具体应用。

1. deadcode_removal.ipynb — 基本死代码移除

目标

手工定义一段含死代码的汇编片段,使用 Miasm 的 DeadRemoval pass 将其消除。

汇编代码片段

; 含死代码的汇编片段
MOV ECX, 0x23      ; 死代码:ECX 被赋值 0x23,但在下一条指令中被覆盖
MOV ECX, 0x4       ; ECX 被重新赋值为 0x4,前一条 MOV ECX, 0x23 永不被使用

MOV ECX, 0x23 是死代码,因为 ECX 在被使用前就被 MOV ECX, 0x4 覆盖。

完整代码

from miasm.analysis.machine import Machine
from miasm.core.parse_asm import parse_txt
from miasm.analysis.data_flow import DeadRemoval
from miasm.core.locationdb import LocationDB# 1. 初始化
loc_db = LocationDB()
machine = Machine('x86_32')
mn_x86 = machine.mn()# 2. 定义汇编代码(含死代码)
assembly = """
main:MOV ECX, 0x23MOV ECX, 0x4RET
"""# 3. 解析汇编文本,生成 AsmCFG
asmcfg, loc_db = parse_txt(mn_x86, 'x86_32', loc_db, assembly)# 4. 生成 IRCFG(将 AsmCFG 提升为 IR 控制流图)
# 【更新】Machine('x86_32').ira(loc_db) → Machine('x86_32').lifter_model_call(loc_db)
# 【旧版 v0.1.3】ir_arch = Machine('x86_32').ira(loc_db)
lifter = Machine('x86_32').lifter_model_call(loc_db)
# 【更新】ir_arch.new_ircfg_from_asmcfg() → lifter.new_ircfg_from_asmcfg()
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg)# 5. 查看死代码移除前的 IRCFG
print("=== 移除前 ===")
for block in ircfg.blocks:print(block)# 6. 执行死代码移除
#    DeadRemoval 基于活跃度分析,删除左值不活跃的赋值
#    【更新】DeadRemoval(ir_arch)(ircfg) → DeadRemoval(lifter)(ircfg)
#    【旧版 v0.1.3】DeadRemoval(ir_arch)(ircfg)
DeadRemoval(lifter)(ircfg)# 7. 查看死代码移除后的 IRCFG
print("=== 移除后 ===")
for block in ircfg.blocks:print(block)

执行效果

移除前,IRCFG 包含两条对 ECX 的赋值:

ECX = 0x23      ; 死代码
ECX = 0x4

移除后,ECX = 0x23 被删除,仅保留:

ECX = 0x4

2. deadcode_unremoval.ipynb — 保护死代码不被消除

目标

练习:添加汇编代码,使 MOV ECX, 0x23 无法被死代码消除器移除——即让 ECX = 0x23 变为"活跃"代码。

解答思路

要使 MOV ECX, 0x23 不被消除,需让 ECX(值为 0x23)在后续被使用。但若直接使用 ECX,代码逻辑会改变。关键技巧是:

  1. 引入一个不透明谓词(Opaque Predicate)——一个看似有条件分支、实则永走同一路径的判断。
  2. 在"永不执行"的分支中使用 ECX,使其在数据流分析中"看似活跃"。
  3. 通过 CMP EAX, -1 配合 MUL EDX(EAX = EAX * EAX,结果必非负),使 JNZ 永远跳转,MOV DWORD PTR [0xDEADBEEF], ECX 永不执行。

汇编代码

MOV ECX, 0x23                     ; 原本为死代码,现通过数据依赖保护
MOV EDX, EAX
MUL EDX                           ; EAX = EAX * EAX(无符号乘法,结果 EAX >= 0)
CMP EAX, -1                       ; 比较 EAX 与 -1(0xFFFFFFFF)
JNZ label                         ; EAX 永不为 -1(因平方非负),故永远跳转
MOV DWORD PTR [0xDEADBEEF], ECX   ; 使用 ECX=0x23,使其"看似活跃"(实际永不执行)
label:

保护机制分析

  • MUL EDX 计算 EAX = EAX * EAX,无符号乘法结果始终 >= 0。
  • CMP EAX, -1:-1 的无符号表示为 0xFFFFFFFF(4294967295)。EAX 平方后只有当 EAX = 0xFFFF 或 EAX = 0x10001 等极少数情况才可能等于 0xFFFFFFFF,但在一般输入下不会。
  • JNZ label:若 EAX != -1 则跳转。在绝大多数情况下 EAX != 0xFFFFFFFF,因此几乎总是跳转到 label。
  • MOV DWORD PTR [0xDEADBEEF], ECX:在跳转目标之前的代码,使用 ECX。由于静态分析无法确定 JNZ 一定跳转,它必须保守地认为这条指令可能执行,因此 ECX 在此处"可能被使用",从而变为活跃变量。

使用 Miasm Jitter 验证

通过 Miasm 的 jitter(模拟执行器)验证 MOV DWORD PTR [0xDEADBEEF], ECX 实际不执行:

from miasm.analysis.machine import Machine
from miasm.jitter.csts import PAGE_READ, PAGE_WRITE# 1. 创建 jitter(模拟执行器),使用 python 后端
loc_db = LocationDB()
# 【更新】jitter 参数命名变化:第二个位置参数改为关键字参数 jit_type
# 【旧版 v0.1.3】jitter = Machine('x86_32').jitter(loc_db, 'python')
jitter = Machine('x86_32').jitter(loc_db, jit_type='python')# 2. 将代码写入内存页
addr = 0x400000
code = b'\xb9\x23\x00\x00\x00'  # MOV ECX, 0x23(示例编码)
jitter.add_memory_page(addr, PAGE_READ | PAGE_WRITE, code)# 3. 设置栈上的返回地址(哨兵)
#    当程序执行到该地址时,触发断点停止执行
jitter.push_uint32_t(0x1337beef)# 4. 设置断点回调函数
def code_sentinelle(jitter):"""当执行到 0x1337beef 时停止"""return False  # 返回 False 停止执行jitter.add_breakpoint(0x1337beef, code_sentinelle)# 5. 从入口地址开始执行
entry_addr = addr
jitter.init_run(entry_addr)
jitter.continue_run()# 6. 验证:检查 0xDEADBEEF 处的内存是否被写入
#    若 MOV DWORD PTR [0xDEADBEEF], ECX 未执行,则该地址未被修改
#    证明 JNZ 分支确实跳转,ECX 的写入指令为死代码(但静态分析无法识别)

深层含义

这个练习揭示了一个重要事实:

  • 静态分析的保守性:活跃度分析是保守近似——它认为"可能执行"的指令中的变量都是活跃的,无法识别不透明谓词保护下的"动态死代码"。
  • 静态 vs 动态:静态分析(DeadRemoval)无法消除被不透明谓词保护的死代码;动态分析(jitter 模拟执行)可以验证该代码确实不执行,但无法自动消除。
  • 组合策略:实际反混淆需结合静态数据流分析与符号执行/动态分析,才能处理此类保护。

3. optimizer.ipynb — 真实二进制优化

目标

对真实二进制文件执行常量传播 + 死代码消除的组合优化,模拟编译器优化 pass 的迭代收敛策略。

优化策略

反混淆优化通常不是单步完成的,而是反复迭代多个优化 pass,直到 IRCFG 不再变化(达到不动点):

常量传播 → 死代码移除 → 空块清理 → 常量传播 → 死代码移除 → ...
(循环,直到没有修改 = 收敛)

完整代码

from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.analysis.data_flow import (DeadRemoval,remove_empty_assignblks,
)
from miasm.analysis.cst_propag import propagate_cst_expr
from miasm.core.locationdb import LocationDB# 1. 加载真实二进制文件
loc_db = LocationDB()
with open('target.bin', 'rb') as fstream:cont = Container.from_stream(fstream, loc_db)machine = Machine(cont.arch)# 2. 反汇编并生成 IRCFG
mdis = machine.dis_engine(cont.bin_stream, loc_db=cont.loc_db)
asmcfg = mdis.dis_multiblock(cont.entry_point)
# 【更新】machine.ir() → machine.lifter(),变量名 ir_arch → lifter
# 【旧版 v0.1.3】ir_arch = machine.ir(cont.loc_db)
lifter = machine.lifter(cont.loc_db)
# 【更新】ir_arch.new_ircfg_from_asmcfg() → lifter.new_ircfg_from_asmcfg()
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg)# 3. 常量传播:从入口地址开始传播已知常量
addr = cont.entry_point
init_infos = {}  # 初始已知信息(可预设寄存器值)
# 【更新】propagate_cst_expr(ir_arch, ...) → propagate_cst_expr(lifter, ...)
# 【旧版 v0.1.3】propagate_cst_expr(ir_arch, ircfg, addr, init_infos)
propagate_cst_expr(lifter, ircfg, addr, init_infos)# 4. 迭代优化循环:反复执行死代码移除与空块清理,直到收敛
# 【更新】DeadRemoval(ir_arch) → DeadRemoval(lifter)
# 【旧版 v0.1.3】deadrm = DeadRemoval(ir_arch)  # 创建死代码移除 pass 实例
deadrm = DeadRemoval(lifter)  # 创建死代码移除 pass 实例modified = True
iteration = 0
while modified:modified = Falseiteration += 1print(f"--- 优化迭代 #{iteration} ---")# 4a. 死代码移除:基于活跃度分析删除非活跃赋值#     返回是否发生了修改modified |= deadrm(ircfg)# 4b. 移除空赋值块:删除不含任何赋值的 AssignBlock#     清理由死代码移除产生的空块modified |= remove_empty_assignblks(ircfg)# (可选)可在此处加入更多 pass:# modified |= merge_blocks(ircfg)          # 合并相邻基本块# 【更新】propagate_cst_expr(lifter, ircfg, ...)  # 再次常量传播# 【旧版 v0.1.3】# propagate_cst_expr(ir_arch, ircfg, ...)  # 再次常量传播print(f"优化完成,共迭代 {iteration} 次")

技术要点

  1. 迭代收敛策略(Iterative Fixpoint Computation)

    • 单次执行常量传播可能产生新的死代码(如传播后发现某赋值的右值不再被使用)。
    • 死代码移除可能暴露新的常量传播机会(如移除中间赋值后,常量可传播到更远的使用点)。
    • 因此需反复迭代,直到一轮迭代中没有任何修改(modified == False),即达到不动点(Fixpoint)
  2. 模拟编译器优化 pass 的流水线

    • 编译器后端的优化器正是以"迭代 pass 直到收敛"的方式工作。
    • Miasm 的 DeadRemovalpropagate_cst_exprremove_empty_assignblks 对应编译器中的 DCEConstantPropagationDeadCodeElimination pass。
    • 这再次印证"反混淆 = 对混淆代码重新施加编译器优化"的核心洞察。
  3. modified |= 的含义

    • 每个 pass 返回布尔值,表示是否修改了 IRCFG。
    • |= 累积修改标志,任一 pass 修改即触发下一轮迭代。

优化效果示例

=== 优化前(混淆代码的 IR)===
EAX = 0x5
EBX = EAX + 0x3        ; EBX = 8(可常量传播)
ECX = 0x23              ; 死代码(ECX 未被使用)
EDX = EBX * 0x2         ; EDX = 16(可常量折叠)
EAX = EDX - 0x6         ; EAX = 10=== 第 1 轮:常量传播 ===
EAX = 0x5
EBX = 0x8               ; 传播 EAX=5 → EBX=5+3=8
ECX = 0x23
EDX = 0x10              ; 折叠 EBX=8 → EDX=8*2=16=0x10
EAX = 0xA               ; 折叠 EDX=16 → EAX=16-6=10=0xA=== 第 2 轮:死代码移除 ===
EAX = 0xA               ; ECX=0x23 被移除(死代码); EBX, EDX 被移除(传播后不再被使用)=== 第 3 轮:无修改,收敛 ===
优化完成,共迭代 3 次

总结

技术 分析方向 编译器应用 反混淆应用
可达定义分析 前向(Forward) 常量传播/折叠 破解指令替换、字面量编码
活跃度分析 后向(Backward) 死代码消除、寄存器分配 消除垃圾代码、死代码插入
迭代优化循环 双向交替 编译器优化 pipeline 反混淆优化 pipeline

核心洞察再次确认:数据流分析技术是编译器与二进制分析的共同基石。理解编译器优化原理,即掌握了反混淆的核心方法论。