Skip to content

方法Method

混合执行

Concolic execution · Concolic testing · Dynamic symbolic execution · 动态符号执行

沿具体运行同步积累符号条件,翻转路径前缀并求新输入重放,说明覆盖保证依赖精确建模、完整调度和查询结果。

形式陈述 ​

一次运行,两份状态 ​

混合执行把 concrete 与 symbolic 合在一起:程序按一个真实输入 a 执行,同时维护符号执行的变量表达式 σ。本文先沿用纯、总定义、无循环、确定性的数学整数模型;环境不随重跑变化,也没有堆或外部调用。

在实际状态 ρ 与符号状态之间维护

ρ(v)=[[σ(v)]]a.

赋值同时计算具体值和符号右侧。遇到判断时,实际值决定本轮方向;符号一侧保存判断表达式及所取方向。设本轮有 m 个判断,按执行顺序把实际所取的带符号条件写成 L1,…,Lm:取真时 Lj=Bj,取假时 Lj=¬Bj。

要改变第 j 次分支而保持此前前缀,查询

Ψj=Φ0∧L1∧⋯∧Lj−1∧¬Lj,

其中 Φ0 是输入前置。不保留旧路径的后缀 Lj+1,…,Lm,因为改变分支后,后续控制位置和值可能完全不同。SAT 模型作为新输入重新执行原程序,并核对实际前缀;UNSAT 只排除这一前缀;unknown 留作未完成目标。[1, §§3.4–3.5]

一个明确的调度版本 ​

保持输入队列、已实际覆盖的判断前缀集合、已完成求解的翻转目标集合。每次运行后,登记本次路径全部前缀,再从最深判断向前考虑翻转;已覆盖或已完成的目标跳过,其余求解。SAT 输入放入队列,UNSAT 目标结束,unknown 单独保留,不能登记成不可行。

这里的前缀身份包含有序的节点与分支方向;含循环的实现还需区分第几次出现。本页无环语言中,每个节点每轮至多出现一次。缓存也必须绑定程序版本、输入域和语义,不能把上次版本的 UNSAT 用在新程序上。

在精确符号模型、完备判定器和穷尽此调度的条件下,无环程序的有限路径树最终全部覆盖:若仍有可行叶未覆盖,在全部已覆盖路径中选一条与它共享前缀最长的路径。两者下一分支不同,对应翻转目标可满足,应已产生一轮覆盖这个更长共同前缀的新执行,与最长选择矛盾。删掉任一条件后,通常只能报告已覆盖部分。

直觉

沿已走过的路找岔口 ​

随机测试可能长期碰不到 y==x+1。混合执行先用普通输入走一遍,记下“为什么走了这一边”;下一轮有针对性地求一个走另一边的输入。具体运行提供真实控制轨迹,符号条件提供改变输入的方向。

它不需要每次把两条分支的完整程序状态都同时留在内存中,但仍可能探索指数多条路径。减少同时存活状态与消除路径爆炸是不同事情。

例子与边界

从零输入走到断言失败 ​

使用前页返回值程序,输入域先固定为 D=[−2,2]2。为了可复算,求解器按字典序选择首个满足输入,调度优先翻转最深未覆盖前缀。

轮次 输入 实际分支 结果与下一目标
1 (0,0) 4F 返回0;翻转为 X>0
2 (1,−2) 4T、5F 返回2;翻转为 X>0∧Y=X+1
3 (1,2) 4T、5T 返回1,违反 return != 1

第二轮具体 a=2,符号 a=X+1;二者在 (1,−2) 上一致。第三轮输入不是“把 y 随便加大”,而是求解第二个判断的否定后得到。返回1在原程序上重放也成立,所以它是实际失败输入。

三轮分别到达三条可行路径,最终每个未走过的翻转都已被覆盖或判不可行。这里恰好覆盖全部路径,不能从“通常运行三轮”推广出其他程序的覆盖界。

为什么必须删掉旧后缀 ​

设另一程序先判断 x==0;真分支令 a:=1,假分支令 a:=2;汇合后再次判断 x==0。从 x=0 运行,两个条件都为真。若翻转首判断,却强留第二个旧条件,公式变为 X≠0∧X=0,错误地把有实际运行的目标当成 UNSAT。

正确目标只约束 X≠0,让新运行自行计算 a=2 并走第二判断的假边。若后续判断还读取分支中被改写的变量,旧后缀也可能带有旧执行的表达式版本;改变前缀后更不能原样沿用。

具体化近似可能漏掉路径 ​

真实工具可能遇到后端不能处理的表达式,便把某个符号量临时换成当前具体值。[1] 这有助于生成测试,但全称覆盖保证随之丢失。例如从 (X,Y)=(0,0) 测试 X*Y==1,若把 Y 固定成0,翻转查询变成 0⋅X=1,得到 UNSAT;实际 (1,1) 却能走真边。

另一些近似会产生满足近似公式、却走不到目标分支的输入,所以新输入重放必须核对前缀。重放失败应记录为模型或搜索偏差,不能登记目标已覆盖。当前下载实现保持精确表达式并枚举有限域,没有这种具体化优化。

找到错误与证明没有错误 ​

一个成功重放的错误输入只依赖那一次运行,证据很短;“没有错误”还要求覆盖全部允许输入路径。超时、只翻最后一个条件、跳过求解失败和截断循环,都可能留下空白。报告应分列已执行测试、已覆盖前缀、不可行目标、unknown 和尚未探索目标。

对并发、时间、随机性或外部服务,重跑同一输入未必重复旧前缀。要么把这些选择纳入输入和记录,要么把保证缩到实际测试轨迹,不能套用确定性证明。

推论与应用

成本按实际运行与查询分别计算 ​

设完成 r 轮运行,第 i 轮执行 ℓi 条语句、遇到 bi 个判断,则产生至多 ∑ibi 个翻转尝试,去重后通常更少。总时间包括 ∑iℓi 的具体执行、符号表达式处理,以及每个目标的求解与重放;不能仅用“测试次数”表示求解器工作。

使用持久前缀节点时,前缀本体最多按新增分支记录量存储;如果像下载原型那样把每个长度为 j 的前缀都复制成元组,一轮还可能产生 O(bi2) 的复制与存储工作。该原型以容易检查为先,不把共享实现的线性前缀成本冒充实际代码成本。

迁移练习:把输入域收紧为 {−1,0,1}2。仍可走4F和4T、5F,但5T要求 X>0,Y=X+1,域中无解;查询应报告该域内 UNSAT。恢复 [−2,2]2 后 (1,2) 又可行,缓存必须失效。这个变化不会证明原来无界整数程序安全。

参考资料

[1] Koushik Sen、Darko Marinov、Gul Agha,CUTE: A Concolic Unit Testing Engine for C,ESEC/FSE 2005,263–272页;链接为作者技术稿,§§3.4–3.5及Figure 8路径约束求解算法说明具体/符号伴随、分支翻转和受限求解器的具体化。本文不实现论文的指针图模型,另给纯整数有限域的精确版本。

[2] Patrice Godefroid、Nils Klarlund、Koushik Sen,DART: Directed Automated Random Testing,PLDI 2005,213–223页;作者实验室页面说明静态接口提取、随机初始测试与动态引导输入生成三个组成部分。

关系图谱5 个相邻概念 · 1 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组