跳转到内容

Reps-Horwitz-Sagiv IFDS — 把跨过程分析变成图上找路

待复核

IFDS 是一套把跨过程数据流分析变成”图上找路”的方法。日常类比:你想知道城市里某条小巷能不能走到某栋楼,最笨的办法是从地图上一笔一笔模拟走路;IFDS 说”先把每种事实当成一个独立的小城市分身,再问图上能不能从起点走到那里”。

你想分析:在 print(x) 这一行,x 有没有可能是没初始化的?传统做法在跨过程时要么丢精度,要么爆炸。IFDS 把每条程序语句对每个事实的影响展开成一张图的边,然后只需要回答图上的可达性问题

这套方法是今天 Soot、WALA、Heros、Phasar 等静态分析工具的核心引擎,也是 taint 分析、未初始化检测、活变量分析的精确算法。

不理解 IFDS,下面这些事都没法解释:

  • 为什么 Heros / Phasar 这类工业级 taint 分析在百万行代码上还能跑出来——它的”上下文敏感”不是靠暴力枚举调用栈
  • 为什么”调用图 + 数据流”的精确分析有 O(E·D^3) 这个上界——多项式复杂度从哪来
  • 为什么常量传播在 IFDS 里”塞不进去”——必须扩到 IDE 才能处理带权值
  • 为什么过程间分析的”上下文敏感”和”路径敏感”不是同一回事——前者用 summary edges 解决

IFDS 的核心想法可以拆成 三步

  1. 展开成超图:把每个程序点 n 拆成 |D|+1 份分身——每个数据流事实 d 一份,再加一个”恒成立”的 0 号分身。流函数变成”从 (n, d1) 连一条边到 (m, d2),当且仅当 d1 在 n 成立能推出 d2 在 m 成立”。类比:把”地图”按”我携带的物品”分层。

  2. 可达性 = 数据流:原本”事实 d 在 m 是否成立”这个问题,等价于”展开图上从 (start, 0) 到 (m, d) 是否有一条路径”。类比:从家出发,能不能带着钥匙走到办公室——只要图上有路就行。

  3. summary edges 处理 call/return:调用一个函数等于跳进它的子图、再跳回来;IFDS 缓存”从这个 entry 事实进、最终从 exit 出来时事实是什么”作为一条 summary edge,下次同样的入口事实直接复用。类比:黑箱函数第一次进去逛一圈记下”输入 → 输出”清单,之后直接查表。

加在一起叫 tabulation 算法(制表:边算边把 call/return 的 summary 填进表里),最坏 O(E·D^3)。

int x;
foo(); // foo 不写 x
print(x); // x 在这里可能未初始化吗?

IFDS 怎么算:

  • D = 程序里所有变量,事实”v 可能未初始化”
  • 流函数:声明 int x → 把 x 加入;赋值 x = 1 → 从集合移除
  • foo() 的 summary edge 表示”foo 不会给 x 赋值”,所以 x 这条事实穿透 foo

逐部分解释

  • 0 号节点是”恒成立”,从它出发到 (print, x) 这条路径表示 x 仍然可能未初始化
  • summary edge 把 foo 整个过程压缩成一条”x 进 → x 出”
  • 算完只要查图:(print, x) 可达 → 报警
data = read_user_input() # source,污染 data
sanitized = clean(data) # sanitizer,移除污染
write(sanitized) # sink,需要确认未污染

IFDS 把”被污染的变量”当作 D 的元素,按三步查图:

  1. sourceread_user_input() 给 data 建边——事实”data 被污染”从 0 号节点可达
  2. sanitizerclean(data) 切断该事实(流函数返回空集)——到 sanitized 的污染边消失
  3. sink:查图是否还有路径到 (write, data);可达 → 报警,不可达 → 安全

Heros / Phasar 直接用 IFDS 当 taint 引擎——喂 source/sanitizer/sink 三类节点就出报告。

案例 3:复制常量传播为什么用不了 IFDS

Section titled “案例 3:复制常量传播为什么用不了 IFDS”
x = 5;
y = x; // 此时 y = 5
z = y + 1; // z = 6

你想分析”y 在某点是常量几”。逐步看卡在哪:

  1. 输入事实是”x = 5”(带具体数值),输出要变成”y = 5”——数值要从一边抄到另一边
  2. IFDS 要求分配(distributive:每个事实可独立推,合起来等于一起推);这里事实互相绑定,拆不开
  3. 所以要扩到 IDE(Interprocedural Distributive Environment,Sagiv-Reps-Horwitz 1996),让边带权值(micro-functions),仍多项式但更复杂

一句话:IFDS 只适合”集合里有没有某标签”;要传”标签上的数值”就得上 IDE。

  1. call/return 边不能当普通边:必须保证”从某个 call 跳进去,必须从对应的 return 跳回来”——叫 realizable path。算成普通可达就把不可能路径也算进去,精度丢光。

  2. 强行把非分配函数塞进来:常量传播、线性等式分析这类不分配的问题,IFDS 给的不是错答案就是退化成保守。要用 IDE(边带权值的扩展)这类更复杂的框架。

  3. D 取得过大让 D^3 因子炸掉:把”所有变量的幂集”或”所有 alias 集合”当 D 是新手坑——IFDS 假设 D 是有限且每个事实是独立元素,幂集当 D 等于直接放弃多项式。

  4. 忽略全局副作用与堆:IFDS 处理的是”每个事实独立的子集”问题,全局变量、堆别名、间接调用都要先做单独抽象(指针分析、调用图构建)再喂进来,否则 summary edges 算错。

适用

  • 数据流域有限的过程间分析(活变量、可达定义、未初始化、taint)
  • 流函数对 ∪ 或 ∩ 分配的问题
  • 需要上下文敏感但不想用按调用栈深度枚举的暴力方法
  • 想要”精确解”(MOP-on-realizable-paths:所有可实现路径上的交汇解)而非”安全近似”

不适用

  • 数据流域无限或非常大(数值范围分析 → 用抽象解释)
  • 流函数不分配(常量传播 → 用 IDE)
  • 要求路径敏感(每条路径独立 → 用符号执行 / SMT)
  • 高阶函数 / 闭包密集(要先做 control-flow 分析再喂 IFDS)
  • 1973 年:Kildall 在 POPL 提出统一数据流框架(kildall-dataflow),但只处理过程内,跨过程精度差。
  • 1980s:Sharir & Pnueli 提出 functional / call-strings 两种过程间方法,functional 方法算 entry-to-exit summary 但实现复杂。
  • 1995 年:Reps(威斯康星)、Horwitz(威斯康星)、Sagiv(特拉维夫)在 POPL 把”过程间精确数据流”重述为”展开图上的可达性”——理论简单、复杂度多项式、覆盖大量经典分析。
  • 1996 年:三人扩出 IDE 处理常量传播这类带权值问题。
  • 2000s 至今:Soot 的 Heros、IBM 的 WALA、LLVM 上的 Phasar 都用 IFDS/IDE 当核心引擎;Datalog 流派(Soufflé / Doop)走另一条路但表达力相通。
  1. “把分析变成图问题”是一种通用思路——只要能展开成图,就能借用图算法的成熟工具
  2. summary edges 是上下文敏感的关键——不靠枚举调用栈,靠”算一次缓存复用”;再强制 call/return 配对,剔除”理论上可走但实际不通”的路径
  3. 分配性是 IFDS 的硬约束——换来多项式复杂度与精确性;要传数值就扩到 IDE
  4. 理论 → 框架 → 工具链:从 1995 论文到 Heros / Phasar,IFDS 走通了从公式到产品的全链路
  • kildall-dataflow —— Kildall 数据流框架是过程内版本,IFDS 把它扩到跨过程
  • cousot-abstract-interpretation —— 抽象解释是更通用的静态分析理论,IFDS 是它在分配子集问题上的高效特例
  • sagiv-shape-analysis —— Sagiv 后续把同类思路扩到堆形状分析
  • steensgaard-pointer —— 指针分析常作为 IFDS 的前置输入(处理间接调用)
  • ssa —— SSA 是过程内数据流的常用 IR,IFDS 在 supergraph 上互补
  • mycroft-strictness —— 同期同类静态分析典范,思路对照
  • llvm —— Phasar 等现代 IFDS 实现的宿主编译器框架
  • differential-datalog —— DDlog (Differential Datalog) — 输入只改一条,引擎只算受影响的那一小块
  • hoare-logic —— Hoare Logic — 把”程序对不对”变成”数学证明对不对”
  • newsome-taintcheck-2005 —— TaintCheck — 给不可信输入贴追踪标签