Rust 的形式化验证工具Prusti是基于 Viper 验证基础设施的工具,用于验证 Rust 程序的内存安全和功能正确性。它通过扩展 Viper 的分离逻辑能力,支持 Rust 特有的所有权系统、生命周期和并发模型验证。Prusti 的核心功能包括:
本文主要是对prusti的验证分离逻辑的基础知识进行介绍。
Hoare逻辑由计算机科学家Tony Hoare于1969年提出,是一种通过前置条件、程序指令、后置条件三元组来验证程序正确性的形式化方法。其核心表示为:
{ P } C { Q }
其中:
问题:验证赋值语句 x = x + 1
的正确性,假设初始时 x = 5
。
Hoare三元组:
{ x = 5 } x = x + 1 { x = 6 }
验证逻辑:
P
:x = 5
C
:x = x + 1
Q
:x = 6
x = e
,则前置条件需满足 P[e/x]
(将表达式 e
代入 x
后的条件)。这里 e = x + 1
,代入后前置条件应为 (x + 1) = 6
,即 x = 5
,与初始条件一致,故三元组成立。问题:验证条件语句 if (x > 0) x = x - 1 else x = x + 1
,确保执行后 x ≥ 0
。
Hoare三元组:
{ True } if (x > 0) x = x - 1 else x = x + 1 { x ≥ 0 }
验证逻辑:
x > 0
时,执行 x = x - 1
,后置条件为 x - 1 ≥ 0
(即 x ≥ 1
);x ≤ 0
时,执行 x = x + 1
,后置条件为 x + 1 ≥ 0
(即 x ≥ -1
,结合前置条件 x ≤ 0
,得 x ≥ 0
);x ≥ 0
成立。分离逻辑(Separation Logic)由John Reynolds等人于2002年提出,专门用于处理指针、动态内存分配和共享内存的程序验证。它在Hoare逻辑基础上引入了空间断言,解决了传统Hoare逻辑在处理内存时的局限性(如悬空指针、内存泄漏等)。
A * B
为真,表示内存被划分为两个不相交的区域,A
描述一部分内存的状态,B
描述另一部分的状态。emp
表示空内存或无内存分配的状态。x ↦ v
表示指针 x
指向值 v
,且该内存地址有效。问题:验证动态分配内存并赋值的操作 p = malloc(sizeof(int)); *p = 10
。
传统Hoare逻辑的局限性:
无法直接描述内存分配后的指针指向关系,前置条件和后置条件难以准确表达内存状态。
分离逻辑的表示:
emp
(无内存分配)p = malloc(sizeof(int)); *p = 10
p ↦ 10
(指针 p
指向值 10
)验证逻辑:
malloc
操作将 emp
转换为 p ↦ ?
(?
表示未初始化值);*p = 10
将 p ↦ ?
转换为 p ↦ 10
,后置条件成立。问题:验证创建两个链表节点并连接的操作:
struct Node { int data; struct Node* next; };
struct Node* a = malloc(sizeof(struct Node));
struct Node* b = malloc(sizeof(struct Node));
a->data = 1; b->data = 2;
a->next = b; b->next = NULL;
分离逻辑的空间断言:
emp
a
后:a ↦ {data: ?, next: ?}
b
后:(a ↦ {data: ?, next: ?}) * (b ↦ {data: ?, next: ?})
(a ↦ {data: 1, next: b}) * (b ↦ {data: 2, next: NULL})
关键特性:
分离合取 *
确保 a
和 b
指向的内存区域不重叠,避免了传统Hoare逻辑无法处理内存分离的问题。
特性 | Hoare逻辑 | 分离逻辑 |
---|---|---|
处理对象 | 顺序程序、基本变量操作 | 指针、动态内存、数据结构 |
内存模型 | 无显式内存表示,假设变量全局可见 | 显式描述内存分区,支持内存分离 |
核心断言 | 前置/后置条件(纯逻辑表达式) | 空间断言(如 x ↦ v , A * B ) |
典型应用 | 算术运算、条件语句验证 | 链表、树结构、并发程序验证 |
解决的问题 | 程序功能正确性 | 内存安全(悬空指针、泄漏等) |
分离逻辑并非推翻Hoare逻辑,而是通过引入空间维度的断言,将程序验证从“值的逻辑”扩展到“内存结构的逻辑”。它允许开发者:
这种扩展使得分离逻辑在操作系统内核、编译器、嵌入式系统等对内存安全要求极高的领域中成为关键工具,而Hoare逻辑则作为基础,仍广泛应用于无指针的程序验证场景。