摘要
对于程序分析来说,循环是非常难以处理的结构,特别是多种路径相互交错的复杂循环。
文章首先基于在循环条件中的变量的更新和循环路径的执行顺序对多路径的循环进行分类,进而来理解循环的复杂性。
之后,文章提出分析框架Proteus。该框架把循环和我们感兴趣的变量集合作为输入,然后通过分析,可以得到关于这些变量经过循环之后的变化情况的摘要。
文章主要的贡献是利用路径依赖自动机(path dependency automaton, PDA)来捕获路径之间的执行依赖。然后利用DFS遍历PDA来对所有可行的执行路径的影响进行摘要。
实验结果说明Proteus在三个方面有非常重大的应用:
- 能分析出比现有的循环边界分析技术更精确的循环边界
- 显著优于最先进的循环验证工具
- 在1秒钟内对非常深的循环产生测试样例,KLEE或者Pex面对这样的循环时可能会花费更多时间或者失败。
概览
相关定义
循环的流图,flowgraph of the loop
是一个元组: G = ( V , E , v s , v t , v e , l ) G=(V,E,v_s,v_t,v_e,l) G=(V,E,vs,vt,ve,l)
- V:节点的集合
- E:V × V是一个边的集合,链接两个顶点
- 顶点 v s , v t ∈ V v_s, v_t \in V vs,vt∈V,是两个虚拟的节点,用来表示每次循环开始时的开始节点和结束节点
- v e v_e ve是一个虚拟节点,代表退出循环
- l l l是一个函数,为每一条边 e ∈ E e\in E e∈E分配一个指令 l ( e ) l(e) l(e)
如果一个节点的出度是2,并且在这两条边上的指令是布尔条件,那么我们称这个节点为分支节点。
循环路径,loop path
G G G中的一条循环路径 π \pi π是一个有限的节点序列 < v 1 v 2 ⋯ v k > \lt v_1 v_2 \cdots v_k\gt <v1v2⋯vk>,其中 k ≥ 1 k \ge 1 k≥1, ( v i , v i + 1 ) ∈ E , 1 ≤ i < k , v 1 = v s (v_i,v_{i+1}) \in E, 1\le i < k, v_1 = v_s (vi,vi+1)∈E,1≤i<k,v1=vs并且 v k ∈ { v t , v e } v_k \in \lbrace v_t, v_e \rbrace vk∈{vt,ve}。
一个路径被叫做迭代路径(iterative path),如果 v k = v r v_k=v_r vk=vr。
θ π \theta_\pi θπ表示路径 π \pi π的条件,实际上是该路径到达该点之前,所有边上的分支条件的合取。
给定流图G,用 Π G \Pi _G ΠG来表示G中所有边的集合
归纳变量,induction variable
给定流图G,一个变量x被称为归纳变量,如果 ∀ π ∈ Π G \forall \pi \in \Pi_G ∀π∈ΠG,对于路径 π \pi π的任意的 i t h , j t h i^{th},j^{th} ith,jth次迭代,有 Δ π i x = Δ π j x \Delta^{i}_\pi x =\Delta_{\pi}^jx Δπix=Δπjx,否则,我们就称x是非归纳变量。
条件分类
每一条路径的条件都可以被转换成 E ∼ 0 E \sim 0 E∼0。这里 E E E是一个表达式, ∼ ∈ { < , ≤ , > , ≥ , = } \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 ≠ j i \neq j i=j, π i → ∗ π j → ∗ π i \pi_i \rightarrow * \pi_j \rightarrow * \pi_i πi→∗πj→∗πi是 ρ G \rho_G ρG的自己,那么 ρ G \rho_G ρG包含了一个环
- 这个环是**周期的(periodic)**如果执行有类似于 < π k i , ⋯ , π k j , ⋯ > + \lt \pi^{k_i},\cdots,\pi^{k_j},\cdots \gt^+ <πki,⋯,πkj,⋯>+(循环周期是 < π k i , ⋯ , π k j , ⋯ > \lt \pi^{k_i},\cdots,\pi^{k_j},\cdots \gt <πki,⋯,πkj,⋯>)这里 k i k_i ki和 k j k_j kj是常量值,表示 π 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 , Δ s X ) (\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的路径条件, Δ s X \Delta_sX ΔsX代表所有归纳变量在执行完一次 π s \pi_s πs之后数值变化情况的集合。
i n i t ⊆ S init \subseteq S init⊆S是一个初始状态的集合。
a c c e p t ⊆ S accept \subseteq S accept⊆S是accepting state的集合,他们没有后继节点。一个accepting state被叫做exit state,如果他表示一条退出路径;被叫做terminal state如果代表了一条迭代路径。
↪ ⊆ S × S \hookrightarrow \subseteq S\times S ↪⊆S×S是有限的状态转换的集合。我们使用 s i ↪ s j s_i \hookrightarrow s_j si↪sj来表示变换 ( s i , s j ) ⊆ ↪ (s_i,s_j) \subseteq \hookrightarrow (si,sj)⊆↪,并且引入一个变量 k i j k_{ij} kij来作为状态计数(state counter),用来表示 k i j k_{ij} kij次执行 s i s_i si之后,可以从 s i s_i si转换到 s j s_j sj。
形如 s i ↪ s j s_i \hookrightarrow s_j si↪sj的转换都会有一个三元组的转移谓词作为注释 ( ϕ i j , ψ i j , U i j ) (\phi_{ij}, \psi_{ij},U_{ij}) (ϕij,ψij,Uij)。其中:
- ϕ i j \phi_{ij} ϕij是关于 k i j k_{ij} kij的约束
- ψ i j \psi_{ij} ψij是guard condition,当 s i ↪ s j s_i \hookrightarrow s_j si↪sj将被触发时将被满足。
这里 ϕ i j , ψ i j 都 时 变 量 在 进 入 \phi_{ij}, \psi_{ij}都时变量在进入 ϕij,ψij都时变量在进入s_i$之前的条件, U i j U_{ij} Uij是计算 X ′ X' X′的函数, X ′ X' X′表示变量 X X X在执行 k i j k_{ij} kij次状态 s i s_i si后的值。也就是说, X ′ = U i j ( X , k i j ) X'=U_{ij}(X,k_{ij}) X′=Uij(X,kij)。
对于上面的例子,(b)中展示的就是从程序生成的PDA。在这个PDA中:
S
=
{
s
1
.
s
2
,
s
3
}
S=\lbrace s_1.s_2,s_3 \rbrace
S={s1.s2,s3},代表图中的三条路径。
i
n
i
t
=
{
s
1
,
s
2
,
s
3
}
init=\lbrace s_1, s_2, s_3 \rbrace
init={s1,s2,s3}代表了初始的状态(标记为红色),并且
a
c
c
e
p
t
=
{
s
3
}
accept=\lbrace s_3 \rbrace
accept={s3}代表了退出循环的一条路径。
对于
s
1
s_1
s1,它代表路径
π
1
\pi_1
π1,并且路径条件
θ
s
1
\theta_{s_1}
θs1是
x
<
n
∧
z
>
x
x<n \wedge z > x
x<n∧z>x。
变量经过
π
1
\pi_1
π1之后的变化
Δ
s
1
{
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}(这里忽略了经过迭代之后不会变化的变量)。
转换上面的表格表示了转换的谓词。对于转换
s
1
↪
s
2
s_1 \hookrightarrow s_2
s1↪s2,它表明执行
k
12
k_{12}
k12次
s
1
s_1
s1后,状态将变成
s
2
s_2
s2。
第一行表明了对
k
12
k_{12}
k12的约束,第二行是guard condition
ϕ
12
\phi_{12}
ϕ12,
z
<
n
z<n
z<n表明当
z
<
n
z<n
z<n满足时,
s
1
s_1
s1将在执行了
k
12
k_{12}
k12次之后,转换为
s
2
s_2
s2。
方程
U
12
U_{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构建算法
继续使用例子:
考虑转换
s
1
↪
s
2
s_1 \hookrightarrow s_2
s1↪s2。让
k
12
k_{12}
k12为变量计数,第
k
12
−
1
k_{12}-1
k12−1和第
k
12
k_{12}
k12次执行
π
1
\pi_1
π1时,变量的状态分别是
X
k
12
−
1
′
:
{
x
′
=
x
+
k
12
−
1
,
n
′
=
n
,
z
′
=
z
}
X'_{k_{12}-1}:\lbrace x'=x+k_{12} - 1,n'=n,z'=z\rbrace
Xk12−1′:{x′=x+k12−1,n′=n,z′=z},
X
k
12
′
:
{
x
′
=
x
+
k
12
,
n
′
=
n
,
z
′
=
z
}
X'_{k_{12}}:\lbrace x'=x+k_{12},n'=n,z'=z\rbrace
Xk12′:{x′=x+k12,n′=n,z′=z}。接下来,我们将两个路径条件
θ
s
1
\theta_{s_1}
θs1和
θ
s
2
\theta_{s_2}
θs2中的
X
X
X分别用
X
k
12
−
1
′
,
X
k
12
′
X'_{k_{12}-1},X'_{k_{12}}
Xk12−1′,Xk12′进行替换,之后再进行合取,可以得到cond为:
(
x
+
k
12
−
1
<
n
)
∧
(
z
>
x
+
k
12
−
1
)
∧
(
x
+
k
12
<
n
)
∧
(
z
≤
x
+
k
12
)
(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)
经过不等式化简之后,我们可以得到:
ϕ
i
j
:
z
−
x
≤
k
12
<
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)
更新方程
U
12
U_{12}
U12为:
{
x
′
,
n
′
,
z
′
}
=
{
x
+
1
×
k
12
,
n
+
0
×
k
12
,
z
+
0
×
k
12
}
=
{
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对于转换
s
2
↪
s
1
s_2 \hookrightarrow s_1
s2↪s1,并且
φ
21
\varphi_{21}
φ21可以被简化为
t
r
u
e
true
true因为
θ
s
2
\theta_{s_2}
θs2蕴含
x
<
n
x<n
x<n。
分离形式的循环摘要
DLS
给定PDA M,和一些归纳变量X某一条trace τ ∈ M \tau \in M τ∈M的摘要可以被表示为 ( t c τ , X τ ) (t_{c_{\tau}},X_{\tau}) (tcτ,Xτ)。这里:
- t c τ t_{c_{\tau}} tcτ是当trace τ \tau τ有效时需要满足的条件
- X τ X_{\tau} Xτ是在执行完这条trace之后,变量的值
M的循环摘要 S M S_M SM为 ∪ τ ∈ E M { ( t c τ , 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作为输入,在进入循环前的前置条件,并且返回摘要
S
M
S_M
SM。
设X为归纳变量,包含了我们感兴趣的变量,rec是记录当前trace(从初始状态到现在状态)的摘要的map。
算法2对每个初始状态进行遍历,并且对初始状态开始的所有有效的trace做摘要。
算法3使用深度优先搜索来总结每个trace重的每一个转换,直到它到达accepting state。输入时当前的状态
s
i
s_i
si,当前的trace在DFS到达当前状态之前的条件
τ
C
\tau_C
τC,和变量的值
X
′
X'
X′在之前的循环摘要中,以及摘要map rec。
如果
s
i
s_i
si在rec中存在,那么说明存在环,我们要通过周期对其进行摘要。如果
s
i
s_i
si是accepting state,那么对一条trace的摘要就完成了。
如果accepting state是退出状态,兵团且
t
C
t_C
tC满足当前路径的条件,那么
X
′
X'
X′会在最后被更新为
Δ
s
i
X
′
\Delta_{s_{i}}X'
ΔsiX′。如果
s
i
s_i
si是终止状态,那么当前的trace代表无限的执行。所以
X
′
X'
X′会被更新为
Δ
s
i
∞
X
′
\Delta_{s_i}^{\infty}X'
Δsi∞X′。实现时,文章用了符号无限值(symbolic infinite value)去代表无穷的更新。
另一方面,如果 s i s_i si不是accepting state,那么算法会继续对 s i s_i si和他的后继进行摘要。程序会检查当前状态能否转化成它的后续状态,如果可以,那么就进行更新。
PDA | info |
---|---|
从初始状态 s 3 s_3 s3开始,程序退出,变量没有变化,所以trace s 3 s_3 s3的摘要是 ( 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);从初始状态 s 1 s_1 s1开始,初始条件是 x < n ∧ z > x x<n\wedge z>x x<n∧z>x,如果处理后继 s 3 s_3 s3,那么trace condition变成 x < n ∧ z > x ∧ z ≥ n x<n\wedge z>x \wedge z \ge n x<n∧z>x∧z≥n,化简为 x < n ≤ z x<n\le z x<n≤z.变量被更新为 { x ′ = n ∧ z ′ = z ∧ n ′ = n } \lbrace x'=n \wedge z'=z \wedge n' = n \rbrace {x′=n∧z′=z∧n′=n}所以, s 1 ↪ s 3 s_1 \hookrightarrow s_3 s1↪s3的摘要为 ( x < n ≤ z , k 13 = 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)次执行整个环
- 一次执行剩余的部分,蓝色部分
红色部分会被抽象成一个状态,它的执行表示 k l , ⋯ , k j , ⋯ , k i k_l,\cdots,k_j,\cdots,k_i kl,⋯,kj,⋯,ki次执行状态 s l , ⋯ , s j , s i s_l,\cdots,s_j,s_i sl,⋯,sj,si
PDA | summary |
---|---|
从第三行,我们可以得到
k
12
=
k
21
=
1
k_{12}=k_{21}=1
k12=k21=1,并且
s
1
s_1
s1已经出现过。所以我们找到了一个环。下面是对该状态进行缩点之后的结果。将循环变成
s
0
s_0
s0。
最后我们可以得到
s
1
↪
(
s
2
↪
s
1
)
+
↪
s
3
s_1 \hookrightarrow ( s_2 \hookrightarrow s_1)^+\hookrightarrow s_3
s1↪(s2↪s1)+↪s3的摘要是:
x
<
z
<
n
,
k
12
=
z
−
x
∧
k
03
=
n
−
z
∧
x
′
=
n
∧
z
′
=
n
∧
n
′
=
n
x<z<n,k_{12}=z-x \wedge k_{03}=n-z \wedge x'=n \wedge z'=n \wedge n'= n
x<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引入一个计数变量 k i k_i ki,用来计算归纳变量在 k i k_i ki次执行之后的值。
直觉上,假设 π i \pi_i πi可以转移到exit path,那么 ∀ j ≠ i \forall j \ne i ∀j=i,在迭代 k j k_j kj次 π j \pi_j πj和 k i − 1 k_i - 1 ki−1次 π i \pi_i πi之后,恰好满足退出的条件;在迭代 k j k_j kj次 π j \pi_j πj和 k i k_i ki次 π i \pi_i πi之后,将不能满足退出条件。对于程序:
这个例子有随你挑不规则的执行。我们引入 k 1 , k 2 , k 3 k_1,k_2,k_3 k1,k2,k3表示他们的执行次数。
变量经过循环之后可以被表示为 x 1 ′ = x 1 − k 1 , x 2 ′ = x 2 − k 2 , x 3 ′ = x 3 − k 3 x'_1=x_1- k_1,x'_2=x_2-k_2,x'_3=x_3-k_3 x1′=x1−k1,x2′=x2−k2,x3′=x3−k3
对于能够到达exit path的 π 1 \pi_1 π1,假设 k 1 − 1 k_1-1 k1−1次执行能满足退出路径的条件, k 1 k_1 k1次不满足,那么我们可以得到:
x 1 ′ − ( k 1 − 1 ) > 0 ∧ x 2 ′ > 0 ∧ x 3 ′ > 0 ∧ x 1 ′ ≤ 0 ∧ x 2 ′ > 0 ∧ x 3 ′ > 0 x'_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 > 0 x1′−(k1−1)>0∧x2′>0∧x3′>0∧x1′≤0∧x2′>0∧x3′>0
这样我们可以得到 x 1 ′ = 0 ∧ x 2 ′ > 0 ∧ x 3 ′ > 0 x'_1=0 \wedge x'_2 >0 \wedge x'_3>0 x1′=0∧x2′>0∧x3′>0
同样的,如果最后一次执行是 π 2 \pi_2 π2或者 π 3 \pi_3 π3,那么可以得到 x 2 ′ = 0 x'_2=0 x2′=0或者 x 3 ′ = 0 x'_3=0 x3′=0。
这个摘要可以被用来验证循环结束之后的性质。
讨论
精度
对于Type 1的摘要是精确的。对其他类型是不精确的,因为引入了不归纳的变量,然而在某些应用下,仍然是精确的。
限制
文章主要关注Type 1的摘要。对于非归纳变量的摘要不能很好的处理。
评估
实验的目标是:
-
了解不同类型的循环在真实世界中的分布
-
证明在实际应用中的准确度和效果。
-
loop bound analysis
-
program verification
-
Test Case Generation
-
结论
提出了多路径循环的分类,类了解循环的复杂性。基于分类,构建了路径依赖自动机,和循环分析架构来对不同类型的循环产生摘要。