“基于 SMT 的软件验证可核对路径条件、锁纪律和冲突访问的逻辑义务,但必须把别名、线程交错及内存模型规则编码完整。分析工具帮助证明定理前件,不会取代 SC for DRF 这一语义蕴涵本身。”
形式陈述
验证条件接口
本页的主要任务是证明注解程序的部分正确性及明确生成的安全条件。给定调用者前置条件、程序和后置条件,验证条件生成器产生有限组公式
是否可满足。UNSAT 证明这组验证条件有效;只有再接上生成器相对于源语言的可靠性定理,才能推出程序保证。SAT 给出失败义务的候选状态,可能只是不可达的归纳反例,不能一概当成可重放程序错误。
对无循环、表达式纯粹且总定义的片段,可用最弱自由前置条件写出精确的部分正确性义务
SMT 在布尔结构上加入整数、实数、位向量、数组、未解释函数等理论。选择哪个理论属于程序语义,不只是求解器性能选项。
验证条件生成器必须证明自己的规则与语言语义对应。对顺序组合,它按逆序传递后置;对分支,它在 guard 下分别生成义务;对过程调用,它使用 callee contract 并建立 frame condition。漏掉一种语句或把异常路径当正常返回,会在公式进入求解器之前破坏可靠性。
可把 VC 作为 proof obligation 列表而非一个大合取,便于定位失败;但共享假设必须复制或由增量上下文维护,不能因拆分丢失全局 invariant。
直觉
路径条件的符号轨迹
程序
assume x >= 0
y := x + 1
assert y > 0
产生反例公式
在线性整数算术中 UNSAT,因此断言成立。若 x,y 是固定宽度无符号位向量,最大值加一回绕到零,公式可能 SAT;把机器值错建模为无界整数会漏掉真实错误。
求解器模型还需映射回源变量和路径。一个 SAT 赋值若违反前端未编码的语言规则,只是编码反例,不是程序反例。
模型中的未解释函数只受 congruence 约束:相同输入给相同输出,不具备单调、纯函数以外的领域性质。把库函数编码为 uninterpreted 可以保守保留多种返回,却无法证明依赖其算术语义的性质;若同时遗漏副作用,则可能不再保守。
对浮点,IEEE NaN、符号零和舍入模式需用 floating-point theory 或已证明近似。把浮点比较改成实数比较会错误使用全序和结合律。
例子与边界
分支、SSA 与 phi
符号执行沿分支积累 path condition;static single assignment 给每次赋值新版本,使等式无歧义。汇合处用
符号执行将这一接口展开为位置、变量表达式和路径条件三元状态,手算三条终端路径并重放输入(1,2)。混合执行再从(0,0)的实际运行出发,翻转路径前缀而丢弃旧后缀,逐轮生成覆盖另外两条路径的输入;精确小域枚举与SMT后端的范围分别声明。
若只把两边约束合取,会错误要求同一次执行同时走两条互斥路径;若只取析取而不关联输出版本,又会允许值来自错误分支。
路径枚举可能指数增长。合并公式、bounded unrolling、抽象与 lemma 学习用于控制规模,但都需保持路径条件语义。
堆内存常编码成数组
SSA 版本化还应覆盖 heap:每次写产生
循环与不变式
有限展开只能验证给定迭代界。全局循环证明需要候选不变式
SMT 可检查候选,不自动保证能发现足够强的不变式。template、abstract interpretation、Houdini 或 interpolation 可生成候选,各有完备边界。
Houdini不变式推断给出有限候选表内的执行闭环:以初始化或保持反例剔除候选,因前提变弱重新检查,最终交出归纳合取。其五状态例逐轮删除p0、p1、p2,保留p3、p4;候选合取最大性与推出安全目标是分别核验的两项结论。
终止还需 variant 的良基下降义务;只证明 invariant 是 partial correctness。
函数递归类似循环:contract 作为归纳假设用于递归调用,同时要证明函数体在假设下实现 contract。若递归调用不满足前置,或 termination measure 未下降,不能因调用摘要已知就跳过。
invariant 推断产生的候选可能只在采样路径上成立。交给 SMT 检查初始化和保持,正是把候选升级为证明的关键;若保持失败,模型给出的是归纳反例,不一定是从程序初态可达的真实 bug。
理论组合与 unknown
SMT 理论有各自可判定片段。非线性整数算术、量词、递归函数和浮点混合可能返回 unknown 或超时。unknown 不是 UNSAT,不能当作证明成功。
数组 extensionality、指针别名和内存对象生命周期需精确编码。把两指针默认不同可让证明轻易通过,却排除了真实 alias 行为。
量词通常依赖 instantiation trigger。trigger 太弱会超时,太窄会漏掉所需实例而返回 unknown;若求解器仍返回 SAT/UNSAT,其语义保证应独立于启发式,但某些不完备理论组合只承诺 unknown。工具不能把“没有生成反例实例”当作全称量词已证。
非线性实算术与非线性整数算术性质不同;前者有特定判定程序,后者很快触及不可判定性。把 solver 对某组公式的成功经验写成片段完备性需要正式依据。
求解器内部 bug 也属于可信计算基。proof-producing solver 可导出可检查证明,但理论证明格式覆盖度和独立 checker 仍需说明。
推论与应用
模块摘要与边界
函数调用可用 contract 摘要:调用者证明前置,随后假设后置和 frame condition。摘要漏写全局副作用会让所有调用证明不可靠。
外部库、并发内存模型、未定义行为和编译器转换都需要独立接口。SMT 只判断收到的逻辑公式,不验证公式是否完整表达实际软件。
并发程序若逐线程生成 VC,需 rely–guarantee、权限或原子性证明连接线程干扰。假设其他线程不改共享变量相当于加入错误 frame condition,单线程 SMT 证明不会自动扩展到并发执行。
证书保存应包含 SMT-LIB 输入、求解器版本、选项和结果;随机种子或预处理变化可能影响性能,但已检查 proof certificate 才能缩小对具体二进制的信任。
参考资料
- Leonardo de Moura and Nikolaj Bjørner, “Z3: An Efficient SMT Solver,” TACAS, 2008, pp. 337–340。
- Mike Barnett et al., “Boogie: A Modular Reusable Verifier for Object-Oriented Programs,” FMCO, 2005, pp. 364–387。
- Aaron R. Bradley and Zohar Manna, The Calculus of Computation, Springer, 2007, Chs. 3–8。