初探软件的形式化验证方法

徐逸辰

Oct, 2020

软件的正确性

单元测试

class StackSpec extends AnyFlatSpec with should.Matchers {
  "A stack" should "be empty when it is created" in {
    val stack = Stack[Int]()
    stack.nonEmpty should be (false)
  }
}

“基于属性”的测试

prop_reverse :: Property
prop_reverse =
  property $ do
    xs <- forAll $ Gen.list (Range.linear 0 100) Gen.alpha
    reverse (reverse xs) === xs

本质上是一种“自动化”的单元测试

对于已有错误的程序

  • 进行调试:gdb, lldb
  • coredump:内存空间快照
  • 调试信息:异常事件记录

类型系统!

void printString(char *str) {
  // print the string
}

printString('a') // compiling error!

编译时发现错误

类型系统

类型系统的作用

最简单直接的原因:许多语言的编译器需要通过类型来进行内存分配。

类型 大小
int 4字节
long long 8字节

好的类型系统能做更多

在编译期明确地指出错误!

def train(model, x, edge_index, y):
  # do something meaningful

if name == '__main__':
  train(model, data.x, data.edge_index, data.y)
def train(model, x, edge_index, y, early_stopper):
  # do something meaningful

if name == '__main__':
  train(model, data.x, data.edge_index, data.y)  # Runtime Error

好的类型系统能做更多

def train(model: Model, x: Matrix, edge_index: Matrix, y: Matrix): Double =
    // do things meaningful

train(model, data.x, data.edge_index, data.y)
def train(model: Model, x: Matrix, edge_index: Matrix, y: Matrix, early_stopper: EarlyStopper[Double]): Double =
    // do things meaningful

train(model, data.x, data.edge_index, data.y)  // Compiler Error

程序运行之前发现并定位错误。

更进一步的例子

def similarity[N](x: Matrix[N], y: Matrix[N]): Scalar = 
  x.top dot y

val x: Matrix[N] = ???
val y: Matrix[N] = ???
similarity(x.top, y)  // Compiler Error
            // Matrix[N] expected, but found Matrix[1, N]

在编译期发现程序中的错误:矩阵维数不匹配。

与众不同

  • 单元测试:测试没有通过,程序因某种原因出了错。

  • 异常、coredump:程序在某种实际环境下崩溃了,给出出错原因的线索或排查资料。

  • 类型系统:包含类型错误的程序根本无法编译,给出具体原因:何处的何种类型不匹配,给出期望类型与实际类型。

  • 本质上是通过一些既定的规则,对程序本身进行推理 (reasoning)

对程序进行推理

第一步 建模状态

  • \(Program = Data + Logic\)

  • \(Data\): 程序当前的状态

  • 一种建模的方法:\(f: String \rightarrow Value\)

    // {}
    int x = 1;
    // { "x" -> 1 }
    int y = x + 1;
    // { "y" -> 2, "x" -> 1 }

第二步 关于状态的简单命题

赋值语句

\(\forall st \in State\)

程序在st状态下执行了语句X := expr

状态变换为st'

则可以知道st' = (X -> eval st expr) + st

关于状态的简单命题

上面的命题可以写为

forall st, st =[ X := expr ]=> (X -> eval st expr) + st
{ "X" -> 1 } =[ Y := X + 1 ]=> { "Y" -> 2; "X" -> 1 }

更复杂的例子

对于if语句:

IF cond DO
  stmt1
ELSE
  stmt2
END

更复杂的例子

对于\(\forall st \in State\)

  • 若已知 eval st cond = True

    st =[ stmt1 ]=> st'

    则有 st =[ IF ... END ]=> st'

  • 若已知 eval st cond = False

    st =[ stmt2 ]=> st'

    则有 st =[ IF ... END ]=> st'

\(^2\)复杂的例子

对于while语句:

WHILE cond DO
  stmt
END

对于\(\forall st \in State\)

  • 若已知 eval st cond = False

    则直接有 st =[ WHILE ... END ]=> st

\(^2\)复杂的例子

  • 若已知 eval st cond = True

    st =[ stmt ]=> st'

    st' =[ WHILE ... END ]=> st''

    st =[ WHILE ... END ]=> st''

一个例子

WHILE X > 0 DO
  X := X - 1;
  Y := Y + 1;
END

求证:\(\forall x, y\)

若开始时的状态为 { "X" -> x, "Y" -> y }

在则在执行完程序之后

状态变为 { "X" -> 0, "Y" -> x + y }

第三步 进一步抽象

  • \(\forall st \in State\), st =[ X := expr ]=> ( X -> eval st expr ) + st

  • \(\forall st, st' \in State\), \(\forall P: State \rightarrow Bool\),

    st =[ X := expr ]=> st'

    若有\(P(st[X \rightarrow\)eval st expr\(])\)

    \(P(st')\)

    记为 { P[X -> expr] } X := expr { P }

意义何在

  • 谓词 \(P: State \rightarrow Bool\) \(\Rightarrow\) 描述状态的性质

  • 具体的某一个状态的具体变化 \(\Rightarrow\) 状态的性质的变化

  • 要证明赋值语句之后的状态符合某一性质

    只需要证明初始状态在根据赋值语句进行名字替换之后符合这一性质

霍尔逻辑

我们得到了霍尔逻辑

上面的三元组被称为霍尔三元组

霍尔三元组

\[ \frac {} {\{ P \} skip \{ P \}} \]

\[ \frac {} {\{ P[E/x] \} x := E \{ P \}} \]

\[ \frac {\{P\} S \{Q\} \wedge \{Q\} T \{R\}} {\{ P \} S; T \{ R \}} \]

霍尔三元组

\[ \frac {\{ B \wedge P\} S \{Q\} \wedge \{ \neg B \wedge P\} T \{Q\}} {\{ P \} if \ B \ then \ S \ else \ T \ end \{ Q \}} \]

\[ \frac {\{B \wedge P\} S \{P\}} {\{ P \} while \ B \ do \ S \ end \{ \neg B \wedge P \}} \]

循环不变量!(loop invariant)

再次证明

WHILE X > 0 DO
  X := X - 1;
  Y := Y + 1;
END

求证:\(\forall x, y\)

若开始时的状态为 { "X" -> x, "Y" -> y }

在则在执行完程序之后

状态变为 { "X" -> 0, "Y" -> x + y }

再次证明

{{ X = x /\ Y = y }} ->
{{ X + Y = x + y }}      // <~~ The Loop Invariant!
WHILE X > 0 DO
  {{ X > 0 /\ X + Y = x + y }} ->
  {{ (X - 1) + Y = x - 1 + Y }}
  X := X - 1
  {{ X + Y = x - 1 + y }} ->
  {{ X + (Y + 1) = x - 1 + y + 1 }}
  Y := Y + 1
  {{ X + Y = x - 1 + y + 1 }} ->
  {{ X + Y = x + y }}
END
{{ !(X > 0) /\ X + Y = x + y }} ->  // <~~ X >= 0
{{ X = 0 /\ Y = x + y }}

程序运行之前,从理论上证明程序正确

形式化验证

形式化证明工具

  • 形式化逻辑:对于逻辑命题的形式化定义与推理

    主要工具:递推(归纳)

  • 逻辑验证工具:Coq

    • 描述命题的工具
    • 构造证明的工具

形式化验证的基本方法

  1. 抽象

实际代码

while (x <= 2) {
    x++;
}

形式化验证工具中的数据结构

prog =
    WHILE X <= 2 DO
        X := X + 1;
    END

形式化验证的基本方法

  1. 描述期望的性质

\(\forall n \in \mathbb N_*, n \le 3\)

{ X = n }
prog
{ X = 3 }

形式化验证的基本方法

  1. 构造并验证证明
Theorem while_example :
    {{X ≤ 3}}
  while (X ≤ 2) do
    X := X + 1
  end
    {{X = 3}}.
Proof.
  eapply hoare_consequence_post.
  - apply hoare_while.
    eapply hoare_consequence_pre.
    + apply hoare_asgn.
    + assn_auto''.
  - assn_auto''.
Qed.

结语

形式化验证有什么用?

  • 日常编程:给予我们思考代码执行过程,进行推理的思维方式(循环不变量)

  • 工业应用:验证高成本、高要求软件的正确性

    • 操作系统
    • 编译器 (CompCert)
    • 电路设计

拓展阅读

  • Software Foundations (Volume 1 and 2)

  • Types and Programming Languages

谢谢大家

欢迎加入字母表俱乐部!

QQ群 749999314