Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3. 程序验证: Hoare 逻辑 / 符号执行 / 形式语义

TL;DR

TLA+ 验证"算法设计",Coq 证明"数学命题";**程序验证(Program Verification)**把两者连起来——验证真实代码满足规格。核心工具是 Hoare 逻辑{P} 程序 {Q}:前置条件 P 下运行程序,结束时满足 Q)、符号执行(用符号变量替代输入,穷举路径)、形式语义(程序行为的严格定义)。这一章讲心智模型 + 最小示例,让你理解"为什么编译器/内核/加密代码能被证明",以及符号执行工具(KLEE / CBMC)怎么自动找 bug。

读完应能:

  1. 写出并理解 Hoare 三元组 {P} C {Q},会推导最弱前置条件。
  2. 理解形式语义的三种(操作/指称/公理),知道分别干什么。
  3. 理解符号执行怎么自动找 bug(KLEE / CBMC 原理)。
  4. 说出程序验证在工程(智能合约、加密、内核)里的实际用法。
  5. 理解"验证代码"和"验证设计"的关系(本章 vs 上一章)。

一、Hoare 逻辑

1.1 Hoare 三元组

$$ {P}\ C\ {Q} $$

  • $P$:前置条件(运行 C 前必须为真)
  • $C$:程序(语句)
  • $Q$:后置条件(C 运行结束后必须为真)

读作:"如果 P 在执行前成立,并且 C 正常终止,那么执行后 Q 成立"。

1.2 例子

{x = 2}  x := x + 1  {x = 3}
{true}   x := 0      {x = 0}
{i < n}  i := i + 1  {i <= n}      ← 循环边界

1.3 核心推导规则

赋值公理(最强后置条件规则):

$${P[x \mapsto E]}\ x := E\ {P}$$

把 P 里所有 x 替换成 E。例:

{x + 1 > 0}  x := x + 1  {x > 0}

序列规则

{P} C1 {R}  ∧  {R} C2 {Q}  ⇒  {P} C1; C2 {Q}

条件规则

{P ∧ B} C1 {Q}  ∧  {P ∧ ¬B} C2 {Q}  ⇒  {P} if B then C1 else C2 {Q}

循环不变量(while 的关键):

{P ∧ B} C {P}  ⇒  {P} while B do C {P ∧ ¬B}

$P$ 是循环不变量:每轮循环开始/结束都为真,退出时加 ¬B

note

循环不变量是 Hoare 验证最难的部分(人工)。像"排序后 i ≤ 数组长度"这类。这就是为什么程序验证工具链要配合自动推理。


二、形式语义

2.1 三种语义

语义定义用途
操作语义(Operational)状态如何一步一步变化(转移规则)解释器、模型检查
指称语义(Denotational)程序映射到数学对象(函数)抽象分析、编译器优化
公理语义(Axiomatic)Hoare 逻辑那套规则程序验证

2.2 操作语义示例(小步)

(x := E, σ) → (skip, σ[x ↦ Eval(σ, E)])     \* 状态更新
(skip; C2, σ) → (C2, σ)                      \* 顺序执行

程序 (C, σ) 二元组, 是一步步转换,直到 skip 结束。

2.3 为什么需要形式语义

  • 语义是编译器/验证器/模型的共同地基:没有严格语义,"编译器正确"无法定义。
  • 上一章 CompCert "保持语义"指的就是操作语义在变换前后不改变(观察行为一致)。

三、最弱前置条件(Weakest Precondition)

3.1 定义

给定语句 C 和后置条件 Q,最弱前置条件 wp(C, Q) 是所有使 C 执行后满足 Q 的最宽松条件。

{P} C {Q}  ⟺  P ⇒ wp(C, Q)

3.2 推导示例

# 目标: {?} y := x + 1; x := y * 2 {x > 10}
# 从后往前:
wp(x := y*2, x > 10)   = y*2 > 10  = y > 5
wp(y := x+1, y > 5)    = x+1 > 5   = x > 4
# 结论: {x > 4} y := x + 1; x := y * 2 {x > 10}

这就是程序验证器的核心算法——从后置条件反向推导前置条件,验证"实际前置 P 蕴含 wp"。


四、符号执行(Symbolic Execution)

4.1 与测试的区别

测试:   输入 = 具体值 → 跑 → 检查输出
符号执行: 输入 = 符号变量 x → 跑 → 记录路径条件 → 检查所有路径
  • 符号代替输入值,执行时路径条件是布尔表达式(x > 0x < 100...)。
  • SMT 求解器(Z3)判断路径条件是否可满足 → 自动生成测试用例 / 找不可达路径 / 找 bug。

4.2 例子

int foo(int x) {
    int y = x + 1;
    if (y > 10) return 1;      // 路径1: x+1 > 10 → x > 9
    else return 0;             // 路径2: x+1 <= 10 → x <= 9
    // 符号执行枚举这两条路径, Z3 判断可满足性
}

4.3 工具

工具语言用途
KLEELLVM bitcodeC/C++ 符号执行,自动找 crash / 断言失败
CBMCC/Java有界模型检查 + 符号执行,验证断言
angr二进制逆向/漏洞分析
Manticore二进制/以太坊智能合约 + 二进制分析
Jalangi / ExpoSEJS前端符号执行

4.4 工程价值

  • 自动生成测试:符号执行产出"覆盖所有可达路径"的输入。
  • 找漏洞:数组越界、除零、断言失败自动发现。
  • 智能合约安全:用符号执行 + 模型检查找重入、整数溢出(如 Mythril / Slither)。

五、静态分析:程序验证的"轻量版"

5.1 三档强度

工具强度例子
Lint / 规则检查轻(启发式)golangci-lint, eslint
静态分析(数据流)中(近似)SAST: gosec, semgrep, CodeQL
形式验证重(精确)KLEE, CBMC, VST, seL4
  • 静态分析可能误报(over-approximate),形式验证不误报(但要规格)。
  • 工程上:Lint + SAST 进 CI(便宜),形式验证用于关键组件。

5.2 CodeQL 例子(找 SQL 注入)

import python
from FlaskRequest r, MySQLExec call
where call.getArg(0).getValue().getExpr().(Name)
      .refersTo(r.getArg("query"))
select call, "SQL injection from request param"

六、程序验证 vs 前面章节

TLA+ (设计)        验证算法/协议的性质        [上一章]
定理证明 (Coq)     验证数学命题 / 抽象程序    [上一章]
程序验证 (本章)     验证真实代码满足规格        ← 本章

三者的分工

对象工具
算法/协议Raft / Paxos / 加密协议TLA+ / 模型检查
数学性质编译正确性 / 内核规格Coq / Isabelle
真实代码C 函数 / 智能合约 / 加密实现Hoare / 符号执行 (KLEE/CBMC)

warning

现实是"验证设计 ≠ 验证实现"。Raft 有 TLA+ 证明,但 Go 实现仍然可能 bug——所以要么信任测试,要么把关键实现也做符号执行/静态分析。


七、工程落地建议

什么时候用什么:
  并发协议设计        → TLA+ (设计期, 便宜)
  加密/解析器/内核关键  → 定理证明 + 程序验证 (代价高, 但正确性关键)
  智能合约            → 符号执行 + 静态分析 (Mythril / Slither)
  普通代码            → Lint + SAST + 测试 (工程默认)

tip

务实组合拳(工程现实):

  1. 测试打底(快、全覆盖路径抽样)
  2. Lint + SAST 进 CI(便宜、抓常见)
  3. 符号执行在关键函数上(解析器、序列化、安全边界)
  4. TLA+ 在并发协议设计期(写实现前)
  5. 只有极少数(加密、内核)才值得全定理证明

八、结束 + 速查表

tip

一页快速唤回:

  • Hoare 三元组 {P} C {Q}:P 前置、C 程序、Q 后置。
  • 赋值公理{P[x→E]} x:=E {P}
  • 循环不变量{P∧B} C {P} ⇒ {P} while B C {P∧¬B},找它是难点。
  • 最弱前置条件 wp(C,Q):反向推导,验证器核心算法。
  • 三种语义:操作(步进)/ 指称(数学对象)/ 公理(Hoare 规则)。
  • 符号执行:符号输入 + 路径条件 + SMT 求解器;自动生成测试 / 找 bug。
  • 工具:KLEE(C)/ CBMC(有界模型)/ angr(二进制)/ Mythril(合约)。
  • 三档:Lint < 静态分析 < 形式验证;误报 vs 精确。
  • 分工:TLA+ 验设计、Coq 验数学、本章验真实代码。
  • 务实:测试 + SAST + 符号执行 + TLA+ 组合,全定理证明只在极关键处。

回主目录: 形式化方法卷 README.