SICI · 天机门 · 第二十五章

形式证道

形式化方法与程序验证 · 化神三层

你以为测试通过就是没bug。测试只能证明bug的存在,不能证明bug的不存在——这之间的鸿沟,就是形式化方法要填的。但穷举所有状态?2^n会要你的命。

状态爆炸之雾低语:「测试通过?我赌你找不出'测试通过却仍致命'的案卷。」
序章

雾锁证道门

测试vs验证 · 五份"测试通过"却致命的案卷

证道之门的门楣上刻着四个字:"形式证道。"门后是一片灰雾——由无数条断裂的状态转移边构成,没有五官,只有声音。诺依曼老祖站在门侧,手里捧着一摞案卷。

雾锁证道之门:由断裂状态转移边构成的状态爆炸之雾悬在门楣,诺依曼老祖捧着五份测试通过却致命的案卷
诺依曼老祖 诺依曼老祖

「你要证道,先过这一关——这五份案卷,测试都'通过'了。可它们都杀了人。你能找出'测试通过'和'没有bug'之间的鸿沟吗?」

状态爆炸之雾 状态爆炸之雾

「测试通过了——这四个字,是我最喜欢的笑话。赌一局:你找得出'测试通过却仍致命'的案例吗?我赌你找不全。」

测试vs验证反直觉挑战器

五份案卷下注 → 逐一翻账 → 测试vs验证的本质鸿沟

状态爆炸之雾逼你下注:五份"测试通过"的案卷,哪几份藏着致命bug?
详解 · K25-01 形式化方法的基本思想与必要性

Dijkstra 1970 年的名言是本章地基:「测试只能证明 bug 的存在,不能证明 bug 的不存在。」形式化方法负责补上后半句:用数学写清系统该做什么(规约 specification)与实现做了什么(模型/代码),再证明两者等价——覆盖所有输入、所有状态、所有路径。两件套缺一不可:只有规约没有证明是写文档,只有模型没有规约是无的放矢。台上五份案卷各漏一处:Therac-25 的并发竞态、Ariane 5 的 64 位转 16 位溢出、FDIV 的查表缺陷、Knight 的死代码、737 MAX 的单点失效——共性是测试只采样了输入空间的一角,而 bug 恰在没采样的角落。形式化不是「更狠的测试」:它换了数学基础——从「观察若干样本归纳」换成「对全空间演绎」。代价也随之而来:建模成本高、规约必须精确,这笔账记在 K25-15。

第一幕 · 自信→崩塌

逻辑基石 · 蕴含之误

命题逻辑/谓词逻辑 · 空虚真 · 直觉推理≠形式推理

石径尽头是一面刻满真值表的石壁。诺依曼老祖指着其中一行:"命题'如果P则Q'(P→Q),当P为假时,整个命题为真还是为假?"

诺依曼老祖 诺依曼老祖

「'如果下雨则地湿'——没下雨,这句话为真还是为假?直觉告诉你'无所谓'。但逻辑不这么看。」

状态爆炸之雾 状态爆炸之雾

「猜'不确定'嘛。直觉不会骗你的——直觉就是我。你一旦信了直觉,我就住进来了。」

逻辑基石 · 蕴含真值表组装器

预测三选一 → 填真值表四行 → 量词应用 · 第一崩塌落定

P→Q 当 P=假、Q=真 时,命题的值是?
详解 · K25-02 命题逻辑与谓词逻辑基础

命题逻辑:原子命题 + 连接词 ¬、∧、∨、→、↔,语义就是真值表——石壁上那四行。蕴含 P→Q 只在 P 真且 Q 假时为假;P 假时恒真,即「空虚真」(vacuous truth):没下雨,「下雨则地湿」没有被违反。谓词逻辑再加三样:项、谓词、量词(∀/∃)——表达能力从「命题之间的关系」升到「对象之间的关系」,这才够写程序性质:「∀x∈N, x+1>x」就是谓词逻辑公式,一个反例即推翻。判定性分野:命题逻辑的可满足性(SAT)是 NP 完全但可判定;一阶逻辑的有效性不可判定(丘奇定理,1936)——这是验证工具链必须绕行的暗礁(详见 K25-14)。台上量词题选 ∀ 的理由:x+1>x 对每个自然数都成立,逐个验证不可能,只能全称量化。

第一幕 · 自信→崩塌

状态之海 · 模型检验原理

穷举所有状态 · 逐一验证性质 · 2^n的种子

石壁裂开一道缝,里面是一片无边的状态之海——每个状态是一颗发光的节点,状态转移边把它们连成一张巨网。诺依曼老祖递给你一只计数器。

诺依曼老祖 诺依曼老祖

「模型检验的核心:把系统所有可能状态都列出来,逐一检查你要的性质在每个状态下是否成立。三个布尔变量——有多少种状态?」

状态爆炸之雾 状态爆炸之雾

「8个。不多嘛。再加一个变量呢?再加一个呢?放心,你数得过来。」

状态空间指数爆炸追踪器

拖滑块调变量数 → 看状态数2^n膨胀 → 点缓解策略看效果

3

状态空间膨胀曲线(2^n)

详解 · K25-03 模型检验:原理与状态空间探索

Clarke、Emerson、Sifakis 在 1981 年前后各自奠基,2007 年同获图灵奖。三步走:①建模——系统抽象成 Kripke 结构(状态集 + 转移关系 + 原子命题标记);②规约——性质写成时序逻辑公式;③验证——检查系统所有行为是否满足公式。与定理证明的根本区别是全自动:不需要人会写证明,工具自己遍历;性质被违反时还能吐出反例(counterexample)——一条具体的状态路径,直接可当调试线索。指数来源就在台上:n 个布尔变量,每个状态是变量的一组赋值,共 2^n 个——3 个变量 8 个状态只是起点,每加一个变量翻一倍。模型检验的哲学是「穷举但有反例」,定理证明的哲学是「推理但费人力」——本章两幕主线在这里分岔。

第一幕 · 自信→崩塌

时序之眼 · LTL与CTL

线性时间vs分支时间 · X/F/G/U · A/E路径量词

状态之海深处悬着一面分叉镜——镜中时间不是一条线,而是一棵不断分叉的树。诺依曼老祖指着镜面。

诺依曼老祖 诺依曼老祖

「你想验证'请求最终被响应'。但'最终'是什么意思?在每一条可能的路径上都被响应?还是存在至少一条路径上被响应?自然语言的'最终',在逻辑里有两个截然不同的意思。」

状态爆炸之雾 状态爆炸之雾

「说'最终'就行了,别较真。模糊的规格,就是我的栖息地。」

LTL与CTL时序逻辑公式组装器

切LTL/CTL模式 → 点算子组装公式 → 看路径验证结果

LTL公式:在每条路径上,请求后总会响应

详解 · K25-04 时序逻辑:LTL与CTL的基本公式

程序的性质很少是「现在的」,多是「永远/最终/直到」——时序性质。LTL(线性时序逻辑)沿单条执行路径解释,四个基本算子:X(下一刻)、F(最终◇)、G(总是□)、U(直到);G(req→F ack) 读作「任何时刻的请求,之后总会被响应」。CTL(计算树逻辑)站在分支点看,加路径量词 A(所有路径)/E(存在路径),与算子成对组合:AG、EF、AU……AG(req→AF ack) 语义与上句相同。两者表达能力互不包含(CTL* 才统一两者);工具跟着走:SPIN 收 LTL——公式转 Büchi 自动机,与系统做积自动机查空;NuSMV 收 CTL——在 BDD 上算不动点。分叉镜的寓意:自然语言的「最终」在 LTL/CTL 里是两个不同的公式——模糊的规格就是 bug 的入口。

第二幕 · 挣扎→再崩塌

验证法器 · SPIN与NuSMV

模型检验工具应用 · 互斥协议验证 · FDIV缺陷复盘

石径前方是一座法器阁,阁中悬着两台巨大的机器——一台刻着"SPIN",一台刻着"NuSMV"。诺依曼老祖递来一张模型卷轴。

诺依曼老祖 诺依曼老祖

「互斥协议——两个进程争夺一个资源,不能同时进入临界区。用SPIN验证它。SPIN用LTL,NuSMV用CTL——S3你写的公式,工具替你跑。」

状态爆炸之雾 状态爆炸之雾

「工具?好。让它跑。跑完所有状态——你就'证明'了。你一定觉得,穷举到底就等于证明了吧?」

SPIN与NuSMV验证工具台

切工具标签 → 补完代码模板 → 运行验证 → FDIV缺陷复盘

SPIN · 互斥协议验证

[] (p1.cs == false -> <> p1.cs == true)
详解 · K25-05 模型检验工具:SPIN/NuSMV应用实例

SPIN(Simple Promela INterpreter,Holzmann,Bell Labs):建模语言 Promela(进程 + 消息通道),引擎是 on-the-fly 深度优先——状态边生成边查、不必先建整张图,配偏序归约压状态;用它验证过网络协议与教科书互斥协议,台上那条 LTL 活性公式跑出反例时给出的就是一条「请求后永远等下去」的路径。NuSMV(CMU SMV 的开源重构):语言是模块化状态机,主武器是符号模型检验——BDD 上算不动点,一条 CTL 公式 AG¬(p1.cs∧p2.cs) 就验掉互斥安全性。工程分工经验法则:并发/网络协议 → SPIN;同步硬件与控制状态机 → NuSMV。两者共享模型检验的宿命:状态空间一大,就得回到 S5 的账上。

详解 · K25-11 模型检验在硬件验证中的应用

FDIV 案卷的解剖:Pentium 的 SRT 除法器用查表实现,2048 个表项里有 5 个本应为零的槽位没有正确写入——除数恰好落进这些槽位时,结果从第 4~5 位有效数字开始出错。Intel 耗资 4.75 亿美元全量召回,震动之后硬件成为形式化第一个大规模落地领域:SRT 查表恰是模型检验的理想对象——状态空间规整、规约清晰(IEEE 754 给每一位都下了定义)。此后 Intel/IBM 的浮点单元普遍经过模型检验,「形式化验证浮点单元」成了设计流程的标准操作;等价性检查(证明优化前后电路等价)更是芯片签核流程的一部分。硬件先于软件落地,不是因为硬件更容易,而是硬件的失效成本立即兑现——4.75 亿美元学费买来整个行业的转向。

第二幕 · 挣扎→再崩塌 · 高潮

状态爆炸 · 穷举之极限

2^n指数爆炸 · 宇宙原子数10^80 · 物理不可能

法器阁深处是一台巨大的推杆计数器,刻度从1到100。状态爆炸之雾盘在计数器上,声音甜得发腻。

状态爆炸之雾 状态爆炸之雾

「S2你亲手数过——3个变量8个状态,'不多'。加到10个呢?1024。工具跑得快,多加几个怕什么?推满它。推到100。」

诺依曼老祖 诺依曼老祖

「我没有阻止你。我只是退后一步,看着你。」

状态爆炸推杆 · 2^n物理极限

推滑块到100变量 → 看状态数逼近宇宙原子数 → 第二崩塌落定

10
010^30 (100变量)10^80 宇宙原子数
详解 · K25-06 状态空间爆炸问题与缓解策略

先算账:2^265≈10^80——265 个变量的状态数等于宇宙原子总数;100 个变量遍历约 4×10^13 年,宇宙年龄的约 3000 倍。缓解四法对应台上四张卡:①BDD(二元决策图,Bryant 1986):布尔函数表示成有向无环图、同构子图共享——McMillan 的 SMV 借它把可验证规模推到 10^120 状态量级(Huth & Ryan 的教材口径),「符号模型检验」由此得名;②抽象(abstraction/refinement):把与性质无关的变量抽象掉,在更小的空间验证,反例可疑再细化(CEGAR 循环);③POR(偏序归约):并发系统里交换两个独立事件的顺序不改变性质——只探索真正不同的交错序;④SAT/BMC(有界模型检验,Biere 1999):把「前 k 步内是否违反」编码成 SAT 实例,找反例极快。四法都在压 2^n,但没有一法把它变成多项式——指数本质不灭,这是所有模型检验器的天花板。

第二幕 · 挣扎→再崩塌 · 缓升

证明之塔 · 定理证明方法

Coq/Lean · 推理规则覆盖所有状态 · 四色定理

石壁崩塌处露出一座金色的塔——证明之塔。塔身由一节节金链构成,每一节是一个推理步骤。诺依曼老祖推开塔门。

诺依曼老祖 诺依曼老祖

「模型检验是'走遍每条路'。定理证明是'证明所有路都通向同一个结论'——不用走,用逻辑推。Coq和Lean是证明之塔的法器。」

证明之塔 · Coq证明构造器

点推理规则补完证明链 → Coq终端逐条运行 → 四色定理脉络

证明链:∀ n∈N, n+1 > n

详解 · K25-07 定理证明方法

换路:不遍历状态,用公理 + 推理规则做演绎。核心规则就一条——肯定前件(modus ponens):有 P 和 P→Q 即得 Q;配上数学归纳法,「对任意 n」的证明有了杠杆——这正是「证明所有路都通向同一结论」的含义。四色定理是分水岭:1976 年 Appel 与 Haken 用程序枚举 1936 种不可约构形完成证明,人工无法复核——「计算机证明算不算证明」吵了三十年;2005 年 Gonthier 用 Coq 把整个证明形式化,机器内核逐行检验,争议就此终结。定理证明的真正卖点不是快,是「小内核」(de Bruijn 准则):几千行经过审计的内核可以检验任意长的证明——把信任压缩到一个可读的小核上。证明可以机器辅助生成,但检验只认内核。

详解 · K25-08 交互式定理证明器:Coq/Isabelle/Lean简介

三大家谱。Coq(法国 INRIA,归纳构造演算内核):代表作 CompCert——Leroy 团队证明了 C 编译器每一步优化的语义忠实,编译器 bug 从此不用背锅。Isabelle/HOL(高阶逻辑 + LCF 小内核架构):代表作 seL4——2009 年第一个获得完整功能正确性证明的 OS 微内核,Isabelle 证明代码数十万行、远超内核本身。Lean(de Moura 2013):定理证明与编程一体,mathlib 已是百万行级数学库,陶哲轩用它推动主流数学形式化。工作模式都是「人给战术(tactic),机器做归约」:输入 intros、induction、simpl,proof state 实时刷新,QED 时内核独立检验——构造与检验分离,人管战略,机器管战役与审计。台上 Coq 终端那几条命令,就是这个循环的最小演示。

第二幕 · 挣扎→再崩塌 · 缓升

Hoare之秤 · 程序正确性证明

Hoare三元组{P}S{Q} · 循环不变式 · 证明之伤愈合

证明之塔顶层悬着一架天平——Hoare之秤。左盘是前置条件P,右盘是后置条件Q,中间是程序S。诺依曼老祖指着天平中央的裂缝。

诺依曼老祖 诺依曼老祖

「Hoare三元组 {P} S {Q}——如果执行前P成立,执行后Q成立。但程序有循环。循环的每一圈,你得找一个'不变式'——一个在循环开始前、每一圈结束后都成立的性质。找到它,证明之伤就愈合。」

状态爆炸之雾 状态爆炸之雾

「不变式?循环跑100万圈你也要证100万次吗?——你要是不找不变式,就回去遍历吧。」

Hoare与Curry-Howard推导组装器

看Hoare三元组 → 选正确不变式 → 验证三条件 → 证明之伤愈合

{x = 0 ∧ i = 0} while (i < n) do i := i + 1; x := x + 1 od {x = n}

选择循环不变式 I(循环每一圈都成立的性质)

证明之伤 · 裂缝

S5的裂缝:穷举走不通。循环100万圈,遍历不可能。
S1的裂缝:直觉推理≠形式推理。不变式不是遍历——是证明。

找到正确的不变式,裂缝愈合。

详解 · K25-09 程序正确性证明:Hoare逻辑与不变式

Hoare 1969。三元组 {P}S{Q}:若执行前 P 成立且 S 终止,则执行后 Q 成立。各语句的公理:赋值最反直觉——{Q[E/x]} x:=E {Q},证明要倒着推;顺序复合与条件各配一条规则。循环是难点:要找不变式 I,尽三重义务——①入口成立;②保持(I ∧ 循环体 ⊢ I');③退出推 Q(I ∧ ¬cond ⊢ Q)。台上例题:{x=0 ∧ i=0} while (i<n) {i++; x++} {x=n},正确不变式是 x=i——入口 0=0、保持 x=i→x+1=i+1、退出 i=n 得 x=n;而 x≤n、x≥0 两条「看起来也对」的候选都在第三重义务上失败(推不出 x=n)。找不变式没有算法——本质上它就是证明,受 K25-14 的不可判定性约束;Dafny 能自动检验不变式,但找,还得靠人。

详解 · K25-13 程序验证工具:Dafny/Frama-C/TLA+简介

三件套各守一段。Dafny(微软研究院,Leino):语言级集成——前置条件 requires、后置条件 ensures、循环不变式 invariant 直接写进代码,编译时调 Z3 SMT 求解器消化证明义务,违反时给出具体反例输入,「注解即定理」。Frama-C(法国 CEA):面向 C 遗产代码——ACSL 注解写规格,WP 插件把每条注解变成证明义务,交给 SMT 求解器或 Coq。TLA+(Lamport,2013 图灵奖得主):验证的不是代码是设计——时态动作逻辑写分布式协议规约,TLC 模型检验器查错;AWS 是最大工业用户,用 TLA+ 验证 S3、DynamoDB 等关键服务(CACM 2015 记载其找到了海量测试多年未发现的复制 bug)。共同内核:都是「规约 + 自动消化证明义务」——Hoare 规则被 SMT 求解器批量执行了。

第三幕 · 顿悟→突破

类型即证明 · Curry-Howard与可计算性

命题=类型 · 证明=程序 · 停机问题 · 可计算性边界

Hoare之秤愈合处,两面镜子从虚空中浮现——左镜刻满命题,右镜刻满类型。它们的镜像内容,竟然一模一样。诺依曼老祖推开双镜。

诺依曼老祖 诺依曼老祖

「你看这两面镜子——左边是命题,右边是类型。它们长得一样,因为它们就是同一个东西。这就是Curry-Howard对应。你写的每一行带类型的代码,都是一个证明的碎片。」

Curry-Howard对应与停机问题推导

配对命题↔类型 → 依赖类型证明 → 停机问题矛盾推导

配对:命题 ↔ 类型(点击配对)

详解 · K25-10 类型系统作为证明工具:Curry-Howard对应

镜子两面的对应表:命题↔类型、证明↔程序、证明化简↔求值——台上五张配对卡就是这张表。细看:P∧Q↔积类型 A×B(证明对);P∨Q↔和类型 A+B;P→Q↔函数 A→B(证明变换器)。依赖类型把量词也收进来:∀x.P(x)↔Π 类型(依赖函数),∃x.P(x)↔Σ 类型(依赖对)——Lean 终端里 (h : xs.length>0) 把前提作为参数写进类型,Vector α n 把长度编进类型。两个推论:①强归一化——每个良类型的项都终止,故对应逻辑一致,Coq/Lean 不会证出假命题;②它对应直觉主义逻辑——排中律不是定理(古典逻辑要加 call/cc 类控制算子才进得来)。实践意义:类型检查即证明检验——你写的每行带类型代码都是证明的碎片,类型系统越强,碎片越大。

详解 · K25-14 可计算性理论简介:可判定/不可判定问题

图灵 1936 的自指陷阱(台上六步推导):假设 H(P,x) 能判定停机;构造 D(x)=若 H(x,x) 则死循环否则停机;问 D(D)——两个分支都矛盾,故 H 不存在。停机问题不可判定。归约是主武器:停机⟶程序等价性⟶Rice 定理——程序的一切非平凡语义性质(关乎功能而非语法)都不可判定,连「这个函数是否总返回偶数」也不可判定。对形式化的意义:不存在验证一切的全自动机器——定理证明器必须人机协作(K25-08)、模型检验器必须限系统为有限状态(K25-03)、SMT 求解器只工作在可判定理论片段(线性算术、数组、位向量)。边界之内效率仍有天壤:素性判定(AKS)多项式时间可判定,SAT 是 NP 完全但现代求解器极强——不可判定是悬崖,可判定与高效之间是整个工程的地带。

第三幕 · 最高潮

Boss 战 · 状态爆炸之雾三问

三问对决 · 排除法收网 · 证明而非穷举

回廊灯火齐灭。灰雾从四面八方压过来,凝成一张巨大的、没有五官的脸——由无数条断裂的状态转移边构成。

状态爆炸之雾 状态爆炸之雾

「我是状态爆炸之雾。我不住在错误里——错误救不了我。我住在你'穷举就能证明一切'的傲慢里。你每以为遍历到底就等于证明,我就胖了一圈。三问。答错一题,雾就厚一分。」

渊账累计 0 笔 · 三问全对后清零

详解 · K25-15 形式化方法的局限性与工程实践

三问全对也不代表形式化是银弹——四条边界。①不可判定(K25-14):一般程序验证无全自动,工具都工作在受限片段。②规格本身可能错:证明的是「实现对规约忠实」,规约错了证明跟着错——行话叫 garbage in, gospel out。③建模鸿沟:模型≠实物——证明依赖环境假设(硬件不坏、传感器不失效),737 MAX 的 MCAS 单点失效恰在模型之外,Therac-25 的教训同源。④成本:seL4 的证明代码数倍于内核 C 代码,形式化报价以人年计。所以工程实践是折衷三层混编:轻量形式化(TLA+ 写设计、几小时出结果)+ 测试 + 运行时监控兜底;全形式化留给失效即人命的部件。三问之外还有三道新题:量子程序的叠加态怎么验证、AI 黑箱的决策路径怎么证明、无限状态并发系统的不变式怎么找——答案不在本章,在 ch26 新型计算范式。

第三幕 · 收束

尾声 · 形式证道

化神三层·形式证道·圆满——下一章:新型计算范式

雾散尽时证道门灯火重明,你手里的证明之链微微发光——不是它变回了原样,是它带着所有崩塌与愈合的痕迹,成了另一种完整。诺依曼老祖走过来,却没恭喜你。

诺依曼老祖 诺依曼老祖

「你证道了。不是因为你答对了三问——答对的会忘。是因为你走过的每一次崩塌、每一次愈合,都刻在你手上了。但——你证明的是经典程序。量子程序的叠加态怎么验证?AI黑箱的决策路径怎么证明?并发无限状态系统的不变式怎么找?那里穷举不行,证明也不一定行。去ch26看看吧。」

知识图谱点亮 · 形式证道之悟

点亮全章8块碎片 → 证明链剪影定格 → 境界突破

安全关键应用图谱

详解 · K25-12 形式化方法在安全关键系统中的应用:航空/汽车/核工业

图谱八个节点对应真实战场:航空 TCAS 防撞系统是最早的教科书案例(用 SMV 验证 TCAS II 规约);DO-178C 的 DO-333 附录是「形式化方法替代测试」的官方许可——高等级软件的覆盖率死角可以用证明补。汽车 ISO 26262 把形式验证列为高 ASIL 等级推荐技术。核工业 DCS 遵 IEC 61508 SIL 4;铁路联锁(任何时刻不得两车同路段)是欧洲几十年的传统阵地。协议侧:PCIe、TLS 1.3 的形式化分析已成签发流程标配。系统侧旗舰:seL4(第一个完整功能正确性证明的 OS 内核)与 CompCert(形式化验证的 C 编译器)。共同逻辑:失效代价是人命或巨额召回,验证成本因此可接受。趋势线也在图谱里:协议/硬件(1990s)→ OS/编译器(2000s)→ 云服务设计(2010s,AWS 用 TLA+)——形式化正从「安全关键」向「关键基础设施」扩散。

复盘 · 形式证道主线:从穷举信仰到证明而非穷举

一条主线串起全部场景:S0 测试 vs 验证的鸿沟(五份案卷)→ S1 逻辑基石(空虚真,第一崩塌:直觉推理≠形式推理)→ S2 模型检验原理(2^n 种子)→ S3 时序逻辑(LTL/CTL 分叉)→ S4 工具与硬件战场(SPIN/NuSMV/FDIV)→ S5 状态爆炸(第二崩塌:穷举信仰)→ S6 定理证明(换路:推理而非遍历)→ S7 Hoare 不变式(两道崩塌在此愈合)→ S8 Curry-Howard 与可计算性(统一的镜面与不可逾越的边界)→ S9 三问收网 → S10 图谱收官。两道崩塌都是排除法:直觉不可靠、穷举不可行——排掉两条错路,只剩一条:证明。三个 trade-off 贯穿始终:穷举(自动 vs 指数)、证明(完备 vs 人力)、类型(表达力 vs 复杂度)。化神收官——证道之门后还有量子、AI 黑箱、无限状态三道新题;范式飞升(ch26)接这口气。

交互实验

模型检测状态爆炸

拖动变量数与取值域,观察状态空间 kⁿ 的指数爆炸。