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】标注。
目录
- 可达定义分析(Reachable Definition Analysis)
- 活跃度分析(Liveness Analysis)
- 深层洞察:编译器与二进制分析的统一
- 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,代码逻辑会改变。关键技巧是:
- 引入一个不透明谓词(Opaque Predicate)——一个看似有条件分支、实则永走同一路径的判断。
- 在"永不执行"的分支中使用 ECX,使其在数据流分析中"看似活跃"。
- 通过
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} 次")
技术要点
-
迭代收敛策略(Iterative Fixpoint Computation):
- 单次执行常量传播可能产生新的死代码(如传播后发现某赋值的右值不再被使用)。
- 死代码移除可能暴露新的常量传播机会(如移除中间赋值后,常量可传播到更远的使用点)。
- 因此需反复迭代,直到一轮迭代中没有任何修改(
modified == False),即达到不动点(Fixpoint)。
-
模拟编译器优化 pass 的流水线:
- 编译器后端的优化器正是以"迭代 pass 直到收敛"的方式工作。
- Miasm 的
DeadRemoval、propagate_cst_expr、remove_empty_assignblks对应编译器中的DCE、ConstantPropagation、DeadCodeEliminationpass。 - 这再次印证"反混淆 = 对混淆代码重新施加编译器优化"的核心洞察。
-
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 |
核心洞察再次确认:数据流分析技术是编译器与二进制分析的共同基石。理解编译器优化原理,即掌握了反混淆的核心方法论。