摘要
对于程序分析来说,循环是非常难以处理的结构,特别是多种路径相互交错的复杂循环。
文章首先基于在循环条件中的变量的更新和循环路径的执行顺序对多路径的循环进行分类,进而来理解循环的复杂性。
之后,文章提出分析框架Proteus。该框架把循环和我们感兴趣的变量集合作为输入,然后通过分析,可以得到关于这些变量经过循环之后的变化情况的摘要。
文章主要的贡献是利用路径依赖自动机(path dependency automaton, PDA)来捕获路径之间的执行依赖。然后利用DFS遍历PDA来对所有可行的执行路径的影响进行摘要。
实验结果说明Proteus在三个方面有非常重大的应用:
- 能分析出比现有的循环边界分析技术更精确的循环边界
- 显著优于最先进的循环验证工具
- 在1秒钟内对非常深的循环产生测试样例,KLEE或者Pex面对这样的循环时可能会花费更多时间或者失败。
概览
相关定义
循环的流图,flowgraph of the loop
是一个元组:G=(V,E,vs,vt,ve,l)G=(V,E,v_s,v_t,v_e,l)G=(V,E,vs,vt,ve,l)
- V:节点的集合
- E:V × V是一个边的集合,链接两个顶点
- 顶点vs,vt∈Vv_s, v_t \in Vvs,vt∈V,是两个虚拟的节点,用来表示每次循环开始时的开始节点和结束节点
- vev_eve是一个虚拟节点,代表退出循环
- lll是一个函数,为每一条边e∈Ee\in Ee∈E分配一个指令l(e)l(e)l(e)
如果一个节点的出度是2,并且在这两条边上的指令是布尔条件,那么我们称这个节点为分支节点。
循环路径,loop path
GGG中的一条循环路径π\piπ是一个有限的节点序列<v1v2⋯vk>\lt v_1 v_2 \cdots v_k\gt<v1v2⋯vk>,其中k≥1k \ge 1k≥1, (vi,vi+1)∈E,1≤i<k,v1=vs(v_i,v_{i+1}) \in E, 1\le i < k, v_1 = v_s(vi,vi+1)∈E,1≤i<k,v1=vs并且vk∈{vt,ve}v_k \in \lbrace v_t, v_e \rbracevk∈{vt,ve}。
一个路径被叫做迭代路径(iterative path),如果vk=vrv_k=v_rvk=vr。
θπ\theta_\piθπ表示路径π\piπ的条件,实际上是该路径到达该点之前,所有边上的分支条件的合取。
给定流图G,用ΠG\Pi _GΠG来表示G中所有边的集合
归纳变量,induction variable
给定流图G,一个变量x被称为归纳变量,如果∀π∈ΠG\forall \pi \in \Pi_G∀π∈ΠG,对于路径π\piπ的任意的ith,jthi^{th},j^{th}ith,jth次迭代,有Δπix=Δπjx\Delta^{i}_\pi x =\Delta_{\pi}^jxΔπix=Δπjx,否则,我们就称x是非归纳变量。
条件分类
每一条路径的条件都可以被转换成E∼0E \sim 0E∼0。这里EEE是一个表达式,∼∈{<,≤,>,≥,=}\sim \in \lbrace <, \le, >, \ge, =\rbrace∼∈{<,≤,>,≥,=}。
- IV Condition。如果E是一个归纳变量,那么这是一个IV体哦阿健
- NIV Condition。如果E不是一个归纳变量
循环执行,loop execution
给定流图G和前置条件,循环执行ρG\rho_GρG是一个路径序列<π1,π2,⋯ ,πi,⋯>\lt \pi_1, \pi_2,\cdots,\pi_i, \cdots\gt<π1,π2,⋯,πi,⋯>,且∀i≥1,πi∈ΠG\forall i \ge 1, \pi_i \in \Pi_G∀i≥1,πi∈ΠG。
- 我们用πi…∗πj\pi_i \ldots * \pi_jπi…∗πj表示在ρG\rho_GρG中从πi\pi_iπi到πj\pi_jπj的部分。
- 我们使用循环执行(loop execution)的概念去定义路径间交互的模式。循环的前置条件说明了在进入循环前,变量的所满足的一些约束。在不同的前置条件下,循环可能会有不同的执行。
- 如果存在i≠ji \neq ji=j,πi→∗πj→∗πi\pi_i \rightarrow * \pi_j \rightarrow * \pi_iπi→∗πj→∗πi是ρG\rho_GρG的自己,那么ρG\rho_GρG包含了一个环
- 这个环是**周期的(periodic)**如果执行有类似于<πki,⋯ ,πkj,⋯>+\lt \pi^{k_i},\cdots,\pi^{k_j},\cdots \gt^+<πki,⋯,πkj,⋯>+(循环周期是<πki,⋯ ,πkj,⋯>\lt \pi^{k_i},\cdots,\pi^{k_j},\cdots \gt<πki,⋯,πkj,⋯>)这里kik_iki和kjk_jkj是常量值,表示πi\pi_iπi和πj\pi_jπj的执行次数
- 给定循环流图G,我们可以将循环执行ρG\rho_GρG分成三种类型:
- 序列执行(Sequential Execution)。如果ρG\rho_GρG不包含任何环,他是序列执行
- 周期执行(Periodic Execution)。如果ρG\rho_GρG中的所有环都是周期的,那么他是周期执行。
- 不规律执行(Irregular Execution)。如果一个循环执行既不是周期的又不是序列的,那么我们把他叫做不规律的执行。这种情况下,循环包括环,但是路径之间相互作用的模式不能被静态的决定,所以,我们不能很容易的知道每条路径的循环次数。
循环分类,Loop classification
循环可以进行如下分类:
IV condition(∀\forall∀) | NIV condition(∃\exists∃) | |
---|---|---|
Sequential | Type 1 | Type3 |
Periodic | Type1 | Type3 |
Irregular | Type2 | Type4 |
Path Dependency Automaton
Path dependency automaton(PDA)是一个4元组M=(S, init, accept ,↪)M=(S, \text { init, accept }, \hookrightarrow)M=(S, init, accept ,↪)
S是一个有限的状态(state)集合,其中每一个元素都代表了ΠG\Pi_GΠG中的一条路径。每个状态是一个三元组(πs,θs,ΔsX)(\pi_s,\theta_s,\Delta_sX)(πs,θs,ΔsX),其中πs∈ΠG\pi_s \in \Pi_Gπs∈ΠG代表了对应的路径,θs\theta_sθs是πs\pi_sπs的路径条件,ΔsX\Delta_sXΔsX代表所有归纳变量在执行完一次πs\pi_sπs之后数值变化情况的集合。
init⊆Sinit \subseteq Sinit⊆S是一个初始状态的集合。
accept⊆Saccept \subseteq Saccept⊆S是accepting state的集合,他们没有后继节点。一个accepting state被叫做exit state,如果他表示一条退出路径;被叫做terminal state如果代表了一条迭代路径。
↪⊆S×S\hookrightarrow \subseteq S\times S↪⊆S×S是有限的状态转换的集合。我们使用si↪sjs_i \hookrightarrow s_jsi↪sj来表示变换(si,sj)⊆↪(s_i,s_j) \subseteq \hookrightarrow(si,sj)⊆↪,并且引入一个变量kijk_{ij}kij来作为状态计数(state counter),用来表示kijk_{ij}kij次执行sis_isi之后,可以从sis_isi转换到sjs_jsj。
形如si↪sjs_i \hookrightarrow s_jsi↪sj的转换都会有一个三元组的转移谓词作为注释(ϕij,ψij,Uij)(\phi_{ij}, \psi_{ij},U_{ij})(ϕij,ψij,Uij)。其中:
- ϕij\phi_{ij}ϕij是关于kijk_{ij}kij的约束
- ψij\psi_{ij}ψij是guard condition,当si↪sjs_i \hookrightarrow s_jsi↪sj将被触发时将被满足。
这里ϕij,ψij都时变量在进入\phi_{ij}, \psi_{ij}都时变量在进入ϕij,ψij都时变量在进入s_i$之前的条件, UijU_{ij}Uij是计算X′X'X′的函数,X′X'X′表示变量XXX在执行kijk_{ij}kij次状态sis_isi后的值。也就是说,X′=Uij(X,kij)X'=U_{ij}(X,k_{ij})X′=Uij(X,kij)。
对于上面的例子,(b)中展示的就是从程序生成的PDA。在这个PDA中:
S={s1.s2,s3}S=\lbrace s_1.s_2,s_3 \rbraceS={s1.s2,s3},代表图中的三条路径。
init={s1,s2,s3}init=\lbrace s_1, s_2, s_3 \rbraceinit={s1,s2,s3}代表了初始的状态(标记为红色),并且accept={s3}accept=\lbrace s_3 \rbraceaccept={s3}代表了退出循环的一条路径。
对于s1s_1s1,它代表路径π1\pi_1π1,并且路径条件θs1\theta_{s_1}θs1是x<n∧z>xx<n \wedge z > xx<n∧z>x。
变量经过π1\pi_1π1之后的变化Δs1{x,z,n}={1,0,0}\Delta_{s_1} \lbrace x,z,n\rbrace=\lbrace 1,0,0\rbraceΔs1{x,z,n}={1,0,0}(这里忽略了经过迭代之后不会变化的变量)。
转换上面的表格表示了转换的谓词。对于转换s1↪s2s_1 \hookrightarrow s_2s1↪s2,它表明执行k12k_{12}k12次s1s_1s1后,状态将变成s2s_2s2。
第一行表明了对k12k_{12}k12的约束,第二行是guard condition ϕ12\phi_{12}ϕ12,z<nz<nz<n表明当z<nz<nz<n满足时,s1s_1s1将在执行了k12k_{12}k12次之后,转换为s2s_2s2。
方程U12U_{12}U12为{x′.z′,n′}={z,z,n}\lbrace x'.z',n' \rbrace=\lbrace z,z,n \rbrace{x′.z′,n′}={z,z,n},表明执行之后,x,z,n分别编程类z,z和n。这里表格中的变量x,n,z不是循环前的初始值,而是表示执行转换的源状态之前的值。
PDA构建算法
继续使用例子:
考虑转换s1↪s2s_1 \hookrightarrow s_2s1↪s2。让k12k_{12}k12为变量计数,第k12−1k_{12}-1k12−1和第k12k_{12}k12次执行π1\pi_1π1时,变量的状态分别是Xk12−1′:{x′=x+k12−1,n′=n,z′=z}X'_{k_{12}-1}:\lbrace x'=x+k_{12} - 1,n'=n,z'=z\rbraceXk12−1′:{x′=x+k12−1,n′=n,z′=z},Xk12′:{x′=x+k12,n′=n,z′=z}X'_{k_{12}}:\lbrace x'=x+k_{12},n'=n,z'=z\rbraceXk12′:{x′=x+k12,n′=n,z′=z}。接下来,我们将两个路径条件θs1\theta_{s_1}θs1和θs2\theta_{s_2}θs2中的XXX分别用Xk12−1′,Xk12′X'_{k_{12}-1},X'_{k_{12}}Xk12−1′,Xk12′进行替换,之后再进行合取,可以得到cond为:
(x+k12−1<n)∧(z>x+k12−1)∧(x+k12<n)∧(z≤x+k12)(x+k_{12}-1<n) \wedge (z>x+k_{12} - 1) \wedge (x + k_{12}<n) \wedge (z \le x + k_{12})(x+k12−1<n)∧(z>x+k12−1)∧(x+k12<n)∧(z≤x+k12)
经过不等式化简之后,我们可以得到:
ϕij:z−x≤k12<z−x+1\phi_{ij}:z-x\le k_{12} < z - x + 1ϕij:z−x≤k12<z−x+1
利用这个信息,可以进一步得到简化的cond:
z<n(φ12)z<n(\varphi_{12})z<n(φ12)
更新方程U12U_{12}U12为:
{x′,n′,z′}={x+1×k12,n+0×k12,z+0×k12}={z,n,z}\lbrace x',n',z'\rbrace=\lbrace x+1\times k_{12},n+0\times k_{12},z+0 \times k_{12}\rbrace=\lbrace z,n,z \rbrace{x′,n′,z′}={x+1×k12,n+0×k12,z+0×k12}={z,n,z}
同理,可以得到φ21=x<n\varphi_{21}=x<nφ21=x<n对于转换s2↪s1s_2 \hookrightarrow s_1s2↪s1,并且φ21\varphi_{21}φ21可以被简化为truetruetrue因为θs2\theta_{s_2}θs2蕴含x<nx<nx<n。
分离形式的循环摘要
DLS
给定PDA M,和一些归纳变量X某一条trace τ∈M\tau \in Mτ∈M的摘要可以被表示为(tcτ,Xτ)(t_{c_{\tau}},X_{\tau})(tcτ,Xτ)。这里:
- tcτt_{c_{\tau}}tcτ是当trace τ\tauτ有效时需要满足的条件
- XτX_{\tau}Xτ是在执行完这条trace之后,变量的值
M的循环摘要SMS_MSM为∪τ∈EM{(tcτ,Xτ)}\cup_{\tau \in E_{M}} \lbrace(t_{c_{\tau}},X_{\tau})\rbrace∪τ∈EM{(tcτ,Xτ)},即将所有trace的摘要进行union。
对Type 1型循环的摘要
算法2是对Type 1型循环做摘要的更细节的程序。
使用循环M的PDA作为输入,在进入循环前的前置条件,并且返回摘要SMS_MSM。
设X为归纳变量,包含了我们感兴趣的变量,rec是记录当前trace(从初始状态到现在状态)的摘要的map。
算法2对每个初始状态进行遍历,并且对初始状态开始的所有有效的trace做摘要。
算法3使用深度优先搜索来总结每个trace重的每一个转换,直到它到达accepting state。输入时当前的状态sis_isi,当前的trace在DFS到达当前状态之前的条件τC\tau_CτC,和变量的值X′X'X′在之前的循环摘要中,以及摘要map rec。
如果sis_isi在rec中存在,那么说明存在环,我们要通过周期对其进行摘要。如果sis_isi是accepting state,那么对一条trace的摘要就完成了。
如果accepting state是退出状态,兵团且tCt_CtC满足当前路径的条件,那么X′X'X′会在最后被更新为ΔsiX′\Delta_{s_{i}}X'ΔsiX′。如果sis_isi是终止状态,那么当前的trace代表无限的执行。所以X′X'X′会被更新为Δsi∞X′\Delta_{s_i}^{\infty}X'Δsi∞X′。实现时,文章用了符号无限值(symbolic infinite value)去代表无穷的更新。
另一方面,如果sis_isi不是accepting state,那么算法会继续对sis_isi和他的后继进行摘要。程序会检查当前状态能否转化成它的后续状态,如果可以,那么就进行更新。
PDA | info |
---|---|
![]() | 从初始状态s3s_3s3开始,程序退出,变量没有变化,所以trace s3s_3s3的摘要是(x≥n,x′=x∧z′=z∧n′=n)(x\ge n,x'=x\wedge z'=z \wedge n'=n)(x≥n,x′=x∧z′=z∧n′=n);从初始状态s1s_1s1开始,初始条件是x<n∧z>xx<n\wedge z>xx<n∧z>x,如果处理后继s3s_3s3,那么trace condition变成x<n∧z>x∧z≥nx<n\wedge z>x \wedge z \ge nx<n∧z>x∧z≥n,化简为x<n≤zx<n\le zx<n≤z.变量被更新为{x′=n∧z′=z∧n′=n}\lbrace x'=n \wedge z'=z \wedge n' = n \rbrace{x′=n∧z′=z∧n′=n}所以,s1↪s3s_1 \hookrightarrow s_3s1↪s3的摘要为(x<n≤z,k13=n=x∧x′=n∧z′=z∧n′=n)(x<n\le z, k_{13}=n=x \wedge x'=n \wedge z'=z \wedge n' = n)(x<n≤z,k13=n=x∧x′=n∧z′=z∧n′=n) |
环型摘要
多重相互链接的环如(a)被视为irregular execution。对于(b)这种有周期的环,实际执行路径如©所示。执行包括两部分
- k(≥0)k(\ge0)k(≥0)次执行整个环
- 一次执行剩余的部分,蓝色部分
红色部分会被抽象成一个状态,它的执行表示kl,⋯ ,kj,⋯ ,kik_l,\cdots,k_j,\cdots,k_ikl,⋯,kj,⋯,ki次执行状态sl,⋯ ,sj,sis_l,\cdots,s_j,s_isl,⋯,sj,si
PDA | summary |
---|---|
![]() | ![]() |
从第三行,我们可以得到k12=k21=1k_{12}=k_{21}=1k12=k21=1,并且s1s_1s1已经出现过。所以我们找到了一个环。下面是对该状态进行缩点之后的结果。将循环变成s0s_0s0。
最后我们可以得到s1↪(s2↪s1)+↪s3s_1 \hookrightarrow ( s_2 \hookrightarrow s_1)^+\hookrightarrow s_3s1↪(s2↪s1)+↪s3的摘要是:
x<z<n,k12=z−x∧k03=n−z∧x′=n∧z′=n∧n′=nx<z<n,k_{12}=z-x \wedge k_{03}=n-z \wedge x'=n \wedge z'=n \wedge n'= nx<z<n,k12=z−x∧k03=n−z∧x′=n∧z′=n∧n′=n
对Type 2,3,4类型的摘要
对这些类型提供一些近似方法,使得在某些情况下仍然有效。
NIV Condition
-
如果条件中的变量单调递增或者递减,并且我们只关心路径依赖(例如,在循环边界分析或者终止性分析中),我们将数值的变化都变成加1或者减1。这种近似可以让变量变成一个归纳变量,这样摘要可以用来获得一些安全的结果。
例如程序:
i永远是递增的,如果我们想找到这个循环的边界,我们将i每次都增加1,这样我们可以得到bound LINT,并且这是一个safe 的bound。 -
一些NIV条件来自于数据结构的遍历,这非常难以进行摘要。在边界分析中,文章采用了基于模式的方法来解决这一类问题中的一部分。
例如将
for(;li! = null;li = li ->next)
转换为:
for(;li < size(li);li++)
-
对于基于输入的NIV条件,他们总是可以满足任何迭代,因为他们的值基于输入或者上下文而不是循环的执行,所以我们将他们抽象成true。
例如将
assume(i > 0); while(i >= 0&&v[i] > key) i−−;
由于v[i]>key是一个NIV条件,我们将其转换为true,可以得到:
assume(i > 0); while(i >= 0&&true) i−−;
这样对于路径π1:{}\pi_1:\lbrace \rbraceπ1:{}
我们可以都得到:
这时候,结果仍然是精确的,但是当数据结构在循环之间或循环内更新的时候,可能会导致错误。 -
对于Irregular执行,相互作用的模式非常难以决定,所以不考虑任何两条路径之间的相互作用,而是考虑循环整体的影响。对每一条路径πi\pi_iπi引入一个计数变量kik_iki,用来计算归纳变量在kik_iki次执行之后的值。
直觉上,假设πi\pi_iπi可以转移到exit path,那么∀j≠i\forall j \ne i∀j=i,在迭代kjk_jkj次πj\pi_jπj和ki−1k_i - 1ki−1次πi\pi_iπi之后,恰好满足退出的条件;在迭代kjk_jkj次πj\pi_jπj和kik_iki次πi\pi_iπi之后,将不能满足退出条件。对于程序:
这个例子有随你挑不规则的执行。我们引入k1,k2,k3k_1,k_2,k_3k1,k2,k3表示他们的执行次数。
变量经过循环之后可以被表示为x1′=x1−k1,x2′=x2−k2,x3′=x3−k3x'_1=x_1- k_1,x'_2=x_2-k_2,x'_3=x_3-k_3x1′=x1−k1,x2′=x2−k2,x3′=x3−k3
对于能够到达exit path的π1\pi_1π1,假设k1−1k_1-1k1−1次执行能满足退出路径的条件,k1k_1k1次不满足,那么我们可以得到:
x1′−(k1−1)>0∧x2′>0∧x3′>0∧x1′≤0∧x2′>0∧x3′>0x'_1-(k_1-1)>0 \wedge x'_2>0 \wedge x'_3>0 \wedge x'_1 \le 0 \wedge x'_2 >0 \wedge x'_3 > 0x1′−(k1−1)>0∧x2′>0∧x3′>0∧x1′≤0∧x2′>0∧x3′>0
这样我们可以得到x1′=0∧x2′>0∧x3′>0x'_1=0 \wedge x'_2 >0 \wedge x'_3>0x1′=0∧x2′>0∧x3′>0
同样的,如果最后一次执行是π2\pi_2π2或者π3\pi_3π3,那么可以得到x2′=0x'_2=0x2′=0或者x3′=0x'_3=0x3′=0。
这个摘要可以被用来验证循环结束之后的性质。
讨论
精度
对于Type 1的摘要是精确的。对其他类型是不精确的,因为引入了不归纳的变量,然而在某些应用下,仍然是精确的。
限制
文章主要关注Type 1的摘要。对于非归纳变量的摘要不能很好的处理。
评估
实验的目标是:
-
了解不同类型的循环在真实世界中的分布
-
证明在实际应用中的准确度和效果。
-
loop bound analysis
-
program verification
-
Test Case Generation
-
结论
提出了多路径循环的分类,类了解循环的复杂性。基于分类,构建了路径依赖自动机,和循环分析架构来对不同类型的循环产生摘要。