前言
在上一篇文章中,我们对程序验证有了比较直观和浅显的了解。 本篇文章将介绍更多的基础性知识,进一步加深了解。 本文的核心在于对循环不变式(Loop Invariant)的介绍,以及对可满足性模理论(Satisfiability Module Theory,SMT)的一些直观解释。
基础知识
程序语法
为了方便讨论,我们取C语言的子集(Subset),作为验证的对象语言。 为此,我们对C语言做以下限制:
- 仅允许
int
类型的变量,以及固定长度的数组; - 允许形如
v = expr
或a[i] = expr
的赋值语句,一般if
分支,以及while
循环; - 允许函数调用;
如下的线性搜索函数,满足以上要求,故可以作为我们的验证对象。 在往后的文章中,我们将尝试对该程序的功能正确性进行验证。
int LinearSearch(int[] a, int length, int key} {
int i = 0;
int res = -1;
while (res == -1 && i < length)
{
if (a[i] == key) { res = i; }
else { i = i + 1; }
}
return res;
}
谓词逻辑与可满足性模理论
倘若本科期间学过《离散数学》课程,对于谓词逻辑(Predicate Logic,又称一阶逻辑,First Order Logic,FOL)肯定不会陌生。 谓词逻辑公式包含量词(Quantifier,如
对一般的谓词逻辑而言,给定的谓词
事实上,在判定谓词逻辑公式的可满足性时,构造谓词的解释是一件非常麻烦的事情。 所以,在大多数情况下,我们会在给出公式的同时,将其中的谓词固定解释为某种含义。 例如,我们可以将谓词
关于谓词逻辑的更多内容,在此我们并不展开,仅用几个例子来演示与验证相关的谓词逻辑。 有需要的话,读者可以寻找其他网络材料或教材自行学习。
- 长度为
length
的数组a
为升序排列: - 长度为
length
的数组a
中存在值为key
的元素: - 加法交换律:
- 一般算术:
(永假式,不可满足)
- 基本算数性质:
可满足性模理论(Satisfiability Module Theory,SMT)是指在某些理论片段(Theory Fragment)下(即限定使用某些谓词常项以及函数),对给定一阶逻辑公式可满足性的判断。 这里“模”的意义和数论中的“模运算”类似,即在限制的理论下讨论公式的可满足性。 例如,以上1,2两个公式,属于数组理论(Array Theory),定义了基本算术运算(如
通常,我们说在某些理论下,某个公式是有效的(Valid),当且仅当在任意的赋值下,该公式都为真。 而我们说某个公式是可满足的(Satisfiable),当且仅当该公式存在至少一组成真赋值。
同时,有一点非常重要,即可满足的对偶性(Duality Property)。 令
为一个一阶逻辑公式。是有效的,当且仅当是不可满足的。 例如,想要证明公式5的有效性,我们可以利用对偶性,证明是不可满足的。 也就是证明是不可满足的。 那么,我们对公式进行代入消去,可得。 这个公式直接等值于,故是不可满足的。 此外,相对而言,求解带全称量词()的SMT公式,比求解不含全称量词的公式要难得多。 所以,使用对偶性做适当的转换,也能显著提升公式的求解效率。
一般来说,在使用SMT时,我们会假定存在一个SMT求解器。其输入为一个在某些特定理论片段下的一阶逻辑公式(SMT公式),并输出该公式的可满足性。 倘若公式可满足,还将输出一组令公式可满足的解释。 比如例子中第1个公式是可满足的,可满足的解释可以为length = 3
且a = [1, 2, 3]
; 第2个公式也是可满足的,可满足解释为key = 3
且a = [1, 3]
; 第3个公式是有效的,即永真的,我们可以用对偶性来证明;第4个公式则是不可满足的,即不存在可满足的解。
循环不变式
在上一篇文章中,我们对循环不变式(Loop Invariant)已经有所接触。 我们花了不少的篇幅来解释循环不变式的含义,以及为什么可以用循环不变式来处理循环。 在这一小结,我们将用比较形式化(Formal)的方式,来定义和解释循环不变式。 并对程序验证中的上近似(Over-approximation,也称过近似)思想进行简单阐述。
首先,循环不变式有两条必须满足的性质,即可达性以及归纳性。 其次,在程序验证中,我们一般还要求循环不变式可证明我们需要验证的属性。 我们将使用以下的例子,结合形式化的定义,来介绍这些性质。
int i = 0;
while(i < 10) { // Inv(i) : 0 <= i <= 10
i = i + 1;
}
assert(i == 10);
为了形式化地定义循环不变式,我们先做以下定义:
- 用
来表示程序变量的值,可以被理解成一个由程序中所有变量组成的元组(Tuple),或者向量(Vector);
- 用
表示循环前的代码对程序变量值的约束;
- 用
表示循环不变式对程序变量值的约束;
- 用
(Guard)表表示循环条件对程序变量值的约束;
- 用
表示循环体代码对程序变量值的修改,其中表示被循环体修改后的程序变量的值;
- 用
表示循环后的代码对程序变量值的约束,一般为所需要验证的属性对程序变量值的要求。
在一行的例子中,这些定义分别为:
-
,程序变量仅有
i
,默认为整型变量; -
,循环前的代码将变量
i
的值约束为0
; -
,循环不变式要求
i
在0
到10
之间; -
,循环条件要求
i
的值小于10
; -
,表示循环体对变量的值
i
和执行完循环体代码后的值i'
之间的约束关系; -
,断言属性对变量
i
的值的要求。
为了便于理解以下形式化定义中的公式,我们给读者提供一条建议:
使用集合的维恩图(Venn Diagram)来理解蕴含关系。 我们可以把每个谓词都看作一个集合,
即表示令为真的所有所形成的集合。 那么可以被理解为,即的集合表示被包含在的集合表示中。
有了以上的诸多比较形式化的定义,我们开始定义循环不变式所要满足的性质。
1) 可达性(Reachable)
可达性这个名字的描述有些不准确。 这条性质实质是要求,循环不变式表示的状态集合
在这个例子中,循环头前的代码形成的约束为
2) 归纳性(Inductive)
归纳性是循环不变式最重要的性质,它要求任何被包含在循环不变式中的状态,经过循环后,得到的新状态,仍然落在循环不变式中。 归纳性保证了循环不变式的不变性成立,即在循环的任意多次执行后,循环不变式始终成立。
在这个例子中,循环不变式
我们再用逻辑公式来说明这个例子中的归纳性,式子如下:
经过代入和化简后可得以下的式子,显然是有效的,所以归纳性也满足。
注意,若
3) 可证明性(Provable)
可证明性质要求,在循环不变式中的状态,满足循环退出的条件后,需要满足我们要验证的性质。
事实上,我们可以把验证属性成立也看成是证明集合之间的包含关系。我 们可以用谓词
来表示程序运行到属性断言前的状态集合,用来表示属性断言所约束的状态集合。 那么,我们验证属性成立,实际上是要证明。 由于难以准确描述,所以我们试图寻找一个循环不变式作为 中介,使得。 如此,我们便间接地验证了属性成立。 这就是我们在这一小节开始所说的 上近似思想。 我们寻找一个 便于表示的, 更大的集合(不变式)来 近似原本的集合(程序状态),在更大的集合上 证明属性成立,从而 间接证明在原本的集合上属性也成立。 这一思想,对于程序验证而言, 至关重要。
以下便是不变式可证明性质的逻辑公式表述:
在这个例子中,可证明性质用以下公式表达:
这个式子显然也是有效的,所以我们寻找的不变式
小结
本篇原本还打算介绍程序语义中的一种公理语义,即霍尔逻辑(Floyd-Hoare Logic),但是感觉篇幅太长不太好,所以把这部分留到下一篇。 在本篇中,我们直观地对可满足性理论(SMT)有了更深入的了解,同时对于循环不变式也有了比较形式化的认识。 在此,多谈几点,供大家参考:
- 基于SMT求解的程序验证,实际上将逻辑推理和验证算法进行的解耦合,研究验证算法的人只需要专注验证算法的改进,而不太需要关注后端的逻辑推理过程。这样的思路是可取的,但却导致了目前大多数的验证算法过度依赖SMT求解,以至于SMT求解成为验证算法的性能瓶颈。或许,我们可以有更好的思路。
- 验证算法对于循环不变式也非常依赖,循环不变式一般由人工提供。但目前也有许多自动生成循环不变式(Invariant Synthesis/Generation)的工作,有兴趣的读者可以自行了解。
- SMT实际上包含了诸多的理论,我们粗浅地介绍了数组理论,线性整数运算理论,也有提及非解释函数理论。不同的理论其求解的方法并不相同,有兴趣的读者可以取SMT-LIB的网站上去了解相关内容。
P.S. 读者可以提前思考一下,我们在本篇开头给出的例子程序,具有什么样的循环不变式。 比如说,对下标变量i
而言,其值的不变式是什么? 可以用来验证什么属性?(简单) 再比如说,对于数组a
来说,在循环执行的过程中,又具有什么样的不变式?(较难)
int LinearSearch(int[] a, int length, int key} {
int i = 0;
int res = -1;
while (res == -1 && i < length)
{
if (a[i] == key) { res = i; }
else { i = i + 1; }
}
return res;
}