Abstractions from Proofs — 討論用

原文:POPL 2004 · doi:10.1145/982962.964021 · 開啟原文 PDF ↗

Abstractions from Proofs

POPL 2004 · Henzinger, Jhala, Majumdar, McMillan

一個問題:程式驗證器該記住哪些事實?

1 先自己試一次

這支程式是對的

while (*) {
1:  if (p1) lock();
    if (p1) unlock();
2:  if (p2) lock();
    if (p2) unlock();
    ...
n:  if (pn) lock();
    if (pn) unlock();
}

lockunlock 嚴格交替。

但驗證器會誤報

它說:「找到一條路徑,lock() 之後沒有 unlock()。」

那條路徑:

  1. 進入第一個 if → 上鎖
  2. 進入第二個 if → 沒解鎖

從控制流程看,這條路徑確實存在。

停一下

驗證器要記住什麼事實,才知道這條路走不通?

答案

記住 p1 的值。

$p_1$ 為真 → 第一個 if 進得去 → 第二個 if 也進得去。

兩個 if 的條件是同一個。

但問題還沒完

這裡有 $n$ 個這樣的 if

全部都要記嗎?

$$2^n \text{ 種組合} \quad (n = 30 \Rightarrow \text{十億})$$

再看仔細一點

$p_1$ 這個事實,只在標籤 1 和 2 之間有用

過了標籤 2,那對 lock/unlock 已經配好,$p_1$ 再也用不到。

所以真正的問題是

哪些事實,該在哪個程式位置記住?

這篇論文在回答這個。

而它的答案有點出乎意料——

不要用猜的。去讀「這條路走不通」的那份證明。

2 這個問題為什麼值得解

一段簡短的歷史

年代發生了什麼
1980smodel checking硬體上很成功——狀態有限,窮舉得完
但軟體不行:一個 32 位元整數就有四十億種值
1997 前後predicate abstraction:不追蹤數值,只追蹤是非問句
2000s 初CEGAR 自動化 refinement;SLAM 驗證 Windows 驅動成功
2004這篇predicate 到底該從哪裡來

predicate abstraction

不追蹤變數的確切數值,只追蹤一組 predicate 的真假

三個 predicate → 每個狀態壓成三個位元 → 最多 $2^3 = 8$ 種。

無限變有限,就能窮舉。

代價:資訊流失。而那正是我們要的——抽象就是刻意丟掉不重要的東西。

CEGAR 迴圈

1. 用很少的 predicate 做粗略抽象
2. 在抽象上找通往 ERROR 的路徑
3. 找不到  → 程式安全,結束
4. 找到了  → 把路徑翻成公式,丟給 solver
5. 有解    → 真的 bug
6. 無解    → 誤報。加新的 predicate,回到 2

難的全部集中在第 6 步

「加入新的 predicate」

加哪些?加多少?加在哪裡?

加太少 → 同一條誤報又冒出來,迴圈不終止 加太多 → $2^n$ 爆炸

到 2004 年,這一步仍然靠啟發式規則在猜

3 前人做到哪裡

先看第 4 步在做什麼

把路徑翻成公式,規則只有兩條:

int x = 1;
int y = x + 1;
if (y < 0) { ERROR; }
$$x = 1 \;\wedge\; y = x + 1 \;\wedge\; y < 0$$

路徑走得通 ⟺ 公式有解。

一個陷阱:為什麼要加下標

x = 1;
x = x + 1;

照規則翻成 $x = 1 \wedge x = x + 1$。

但 $x = x + 1$ 永遠是假的

SSA

給每次賦值後的值一個新名字:

$$x_1 = 1 \;\wedge\; x_2 = x_1 + 1$$

有解了($x_1 = 1, x_2 = 2$),而且忠實。

論文寫成 $\langle x, 1 \rangle$。

下標不是指令編號——p.234 說 $\langle \mathit{ctr},1 \rangle$ 是「前兩個指令後」的值。

前人的兩個毛病

一、數量爆炸。 $p_1 \dots p_n$ 全要追蹤 → $2^n$

二、位置放錯。 找到的 predicate 被一視同仁撒到許多、甚至全部的程式位置

但 $p_1$ 只在標籤 1 到 2 之間有用。

論文要的是 parsimonious

在每個控制位置,只指定當下變數之間的關係,而且只留證明正確性真正需要的那些 — Abstract, p.232

這個觀察值多少?

一個 138,000 行的 C 驅動程式:

證明正確總共需要382 個 predicate
每個位置平均只需要約 8

382 對 8。

4 這篇的解法

洞察:證明裡面就有答案

一條抽象路徑之所以走不通,理由已經簡潔地編碼在「它走不通」的證明裡了。 — §1, p.233

solver 說「無解」時,它握有一份反駁證明

讀它,需要的事實就浮出來。而且只要對證明做一次線性掃描

工具:只用共同語言說話

把走不通的路徑切開:前半 $\varphi^-$,後半 $\varphi^+$。

interpolant 是一個公式 $\psi$:

  1. $\varphi^- \Rightarrow \psi$
  2. $\psi \wedge \varphi^+$ 無解
  3. $\psi$ 只用兩邊共同的符號

比喻

前半段講中文,後半段講日文

$\psi$ 只能用兩邊都寫得出來的漢字

第 3 條就是我們要的東西

切點兩側的共同符號 = 執行到切點時,各變數當下的值

不多不少,剛好是那個位置需要記住的事實。

回頭看前人的毛病:predicate 混進了變數的「舊值」——被條件 3 直接擋掉

但貢獻不是「用了 interpolation」

Craig interpolation1957 年的定理(論文參考文獻 [9])。

它只保證 interpolant 存在——沒說怎麼算。

貢獻是一套可以機械執行的規則

第一層:一個證明系統(Fig. 3, p.235)——HYP、COMB、CONTRA、RES

第二層:把每條規則改寫成

$$(\varphi^-, \varphi^+) \vdash \Delta \;[\psi]$$

方括號同步累積。推到矛盾時,裡面剩下的就是 interpolant。

不用猜,不用事後驗證,照著走就會掉出來。

第二個洞察:locality

不要只切一刀。沿路徑每個切點都切一次。

第 $i$ 個切點的 interpolant,只含那裡的共同符號 → 「第 $i$ 個位置需要知道的事實」

於是得到一張,不是一包全域 predicate。

382 對 8 就是這樣來的。

5 動手算

案例一:回看 locking

誤報路徑:進第一個 if、不進第二個。

所以那個切點的 interpolant 不可能牽扯 $p_2 \dots p_n$——那些符號在一側根本不出現。

條件 3 自動做到了「只記必要的」。

案例二:論文的主例(Figure 2, p.234

1:  x := ctr;
2:  ctr := ctr + 1;
3:  y := ctr;
4:  assume(x = m);
5:  assume(y ≠ m + 1);

為什麼走不通?

翻成公式

約束
1$\langle x,1 \rangle = \langle \mathit{ctr},0 \rangle$
2$\langle \mathit{ctr},1 \rangle = \langle \mathit{ctr},0 \rangle + 1$
3$\langle y,2 \rangle = \langle \mathit{ctr},1 \rangle$
4$\langle x,1 \rangle = \langle m,0 \rangle$
5$\langle y,2 \rangle \ne \langle m,0 \rangle + 1$

在第 2 行後面切一刀

$\varphi^-_2$ = 前兩條,$\varphi^+_2$ = 後三條

共同符號:$\langle x,1 \rangle$、$\langle \mathit{ctr},1 \rangle$

也就是「執行完前兩行後,$x$ 和 ctr 的值」。

算出來的 interpolant

$$\psi_2 = (\langle x,1 \rangle = \langle \mathit{ctr},1 \rangle - 1)$$

翻回程式變數:

$$\hat\psi_2 = (x = \mathit{ctr} - 1)$$

位置 2 需要記住的唯一事實。

不是 ctr 的值,不是 x 的值——是它們的差

案例三:現在換你算

用論文 Figure 4(p.237)的例子,照規則自己推一次。

$$\varphi^- = (0 \le y-x) \wedge (0 \le z-y) \qquad \varphi^+ = (0 \le x-z-1)$$

做完之後,一個陷阱

問題一:$y$ 在哪裡消失的? → 兩個不等式相加,$-y$ 和 $+y$ 抵銷。國中代數。

問題二:為什麼保證只含共同變數?不是因為代數消去,是因為整套規則維持的不變量

混為一談的後果:換到 resolution 那半套規則就完全對不上。

6 實驗:他們比了什麼

三組設定,同一個工具

設定意思
Previous舊版 BLAST,不用 interpolant
Craig用 interpolant 找 predicate,但不分位置
Craig+Locality用 interpolant,而且每個位置只追蹤相關的

同一批 Windows 驅動程式、同一台機器。

Table 1(節錄,p.244

程式行數PreviousCraigC+LC+L 的 ReachPredsAvg/Max
floppy17,7077m10s7m56s25m20s0m46s2407.7/37
parclass138,37342m24s77m40s1m6s3827.2/28

= 六小時跑不完

反直覺的一格

Craig+Locality 的總時間反而比 Craig 慢。

floppy:25m20s vs 7m56s

那它贏在哪?

Reach(給定所有 predicate 後,只做可達性分析):

CraigCraig+Locality
floppy3m21s0m46s
parclass9m1s1m6s

快了將近九倍。

因為每個位置只帶 7 個 predicate,不是全部 382 個。

所以「哪個比較好」取決於你在測什麼

論文還提到(表上沒有數字):記憶體顯著較少、證明樹更小。

他們沒有比什麼

7 怎麼讀原文

一組動作,不是一個順序

如果只有一小時

Abstract → §1 → §2(含 Locality 段)→ §3 的 Fig. 3 與 Fig. 4 → §7 的 Table 1

注意:Locality 那段在 p.235,排在「3 Interpolants from Proofs」標題之前,屬於 §2。

回到最初那個問題

哪些事實,該在哪個程式位置記住?

答案:去讀「這條路走不通」的證明。

切點兩側的共同符號,就是那個位置需要記住的東西。

382 對 8。