本文主要介绍V1.19的manual,因为我的frama版本是27,每个frama版本都有对应的名字,可按这个名字找对应的manual。
ACSL - Frama-Chttps://www.frama-c.com/html/acsl.html
后续修改:这个啥玩意manual太难读了,可能旧一点的版本会介绍更基础的东西,所以从chapter2函数合约开始转为介绍由一位长期使用 WP 和 ACSL 进行研究工作的业者所撰写的教程,教程内包括基本语法,用法和与frama-c的交叉。
Chapter 1 Introduction
这章主要是介绍定义和词汇。
规范 Specification
规范直接作为注释写在C语言的源文件的注释中,以便源文件可以一直保持可编译的特性,这些规范(注释)必须以/*@ 或者 //@开头,并像C语言那样注释结尾。例如:
/*@ requires ...
ensures ...
*/
void main() {
....
/*@ assert about_x: x>=0 */
}
规范(注释)的种类
- 全局注释Global annotations
- 函数合约Function Contract:插入在函数的声明或定义前。
- 全局不变量Global invariant
- 类型不变量Type invariant:允许声明结构不变式、联合不变式和由 typedef 引入的类型名不变式。
- 逻辑规范logic specification:通过声明新的逻辑类型、逻辑函数、谓词及其满足的公理来定义逻辑函数或谓词、公理化。这种注释放在全局声明的层次上。
- 语句注释Statement annotations
- Assertation
- loop annotation
- statement contract:与函数契约非常相似,放在语句或代码块之前。必须对语义条件进行检查(例如,goto不得进入内部,goto不得进入外部)
- ghost code
关于关键词
规范语言的其他关键字以反斜杠开头(如\result),如果它们用于术语或谓词的位置。否则,它们不以反斜杠开始(如ensure),并且它们仍然是有效的标识符。
语法符号
我们使用额外的符号e*来表示e的零次,一次或多次出现,e+表示e的一次或多次出现,e?表示e出现0或1次。类似正则。
一些语句说明
(我实在没找到详细介绍的官方文档或书籍,就只记录我在这个过程中看到的关键词)
requires
:指定函数或代码块执行前必须满足的条件。/*@ requires x >= 0 */
ensures
:指定函数或代码块执行后必须满足的条件。/*@ ensures x >= 0 */
assigns
:指定函数或循环中可能被修改的变量集合,这有利于帮助分析工具理解函数的修改范围。
/*@ assigns *result; ensures *result == (a < b ? a : b); ensures \result == \old(a) || \result == \old(b); */ int min(int a, int b, int *result) { if (a < b) { *result = a; else { *result = b; } return *result; }
predicate:用于定义一个可重用的逻辑表达式,这个表达式可以在多个地方被引用。
假设我们有一个函数,它检查一个整数是否在某个范围内。我们可以使用
predicate
来定义这个范围,并在requires
和ensures
子句中使用它。#include <assert.h> typedef struct { int lower; int upper; } Range; /*@ predicate InRange(int x, Range r) = x >= r.lower && x <= r.upper; @*/ /*@ requires InRange(x, \old(range)); ensures InRange(x, range); */ void update_range(Range *range, int x) { if (x < range->lower) { range->lower = x; } else if (x > range->upper) { range->upper = x; } }
在这个例子中:
predicate InRange(int x, Range r) = x >= r.lower && x <= r.upper;
定义了一个名为InRange
的谓词,它接受一个整数x
和一个Range
结构体r
作为参数,并检查x
是否在r.lower
和r.upper
之间。在
update_range
函数的前置条件中,requires InRange(x, \old(range));
使用了InRange
谓词来指定函数的前置条件:参数x
必须在range
结构体的旧值所表示的范围内。
ensures InRange(x, range);
使用了相同的InRange
谓词来指定函数的后置条件:函数执行后,参数x
必须在range
结构体的新值所表示的范围内。通过使用
predicate
语句,我们可以在不同的地方重用InRange
谓词,这使得规范更加简洁和易于维护。如果范围检查的逻辑发生变化,我们只需要更新predicate
的定义,而不需要修改每个使用该谓词的地方。
logic:用于声明一个逻辑表达式,这个表达式可以被后续的规范元素引用。
假设我们有一个简单的程序,它使用一个全局变量
g_state
来表示系统的状态,这个状态可以是0
(关闭)或1
(打开)。我们想要定义一个逻辑表达式来表示系统是打开的状态。/*@ logic bool SystemIsOn(int state) = state == 1; @*/ int g_state = 0; /*@ requires SystemIsOn(g_state); @*/ void turn_on() { g_state = 1; } /*@ requires !SystemIsOn(g_state); @*/ void turn_off() { g_state = 0; }
在这个例子中:
logic bool SystemIsOn(int state) = state == 1;
定义了一个逻辑表达式SystemIsOn
,它接受一个整数参数state
并检查它是否等于1
,即系统是否处于打开状态。在
turn_on
函数的规范中,requires SystemIsOn(g_state);
使用了SystemIsOn
逻辑表达式作为前置条件,但由于我们在函数内部将g_state
设置为1
,这个前置条件实际上是不准确的,正确的前置条件应该是没有限制或者g_state
可以是任意值。在
turn_off
函数的规范中,requires !SystemIsOn(g_state);
使用了SystemIsOn
逻辑表达式的否定作为前置条件,表示在调用turn_off
之前,系统应该是打开的。
behavior
:定义函数的不同行为,每个行为可以有自己的assumes
和ensures
。/*@ behaviors behavior a_is_greater: assumes a > b; ensures \result == a; behavior b_is_greater: assumes b >= a; ensures \result == b; complete behaviors; disjoint behaviors; */ int max(int a, int b) { return a > b ? a : b; }
在这个例子中:
behaviors
关键字开始定义一组行为。
a_is_greater
是第一个行为的名称,它表示当a
大于b
时的行为。
assumes a > b;
指定了触发这个行为的条件,即a
必须大于b
。ensures \result == a;
指定了在这个行为下,函数返回值必须等于a
。
b_is_greater
是第二个行为的名称,它表示当b
大于或等于a
时的行为。
assumes b >= a;
指定了触发这个行为的条件,即b
必须大于或等于a
。ensures \result == b;
指定了在这个行为下,函数返回值必须等于b
。
complete behaviors;
表示这两个行为覆盖了函数的所有可能的调用情况,即要么a
大于b
,要么b
大于或等于a
。
disjoint behaviors;
表示这两个行为是互斥的,即在任何给定的函数调用中,只有一个行为会被触发。通过使用
behavior
语句,我们可以为函数的不同执行路径提供更精确的规范,这有助于静态分析工具更准确地验证函数的正确性。
assumes
:在behavior
中使用,指定触发特定行为的条件。
ensures
:在behavior
中使用,指定在特定行为被触发时,函数保证成立的条件。
complete
:用于behavior
,表明所有列出的行为覆盖了函数的所有可能调用情况。
disjoint
:用于behavior
,表明列出的行为是互斥的。
also
:用于添加额外的后置条件,而不替换原有的后置条件。假设我们有一个函数
increment
,它接受一个整数指针p
并将其值增加1。我们想要确保函数执行后,指针p
指向的值确实增加了1,并且这个新值是正数。/*@ ensures \valid(p); ensures *p == \old(*p) + 1; also ensures *p > 0; */ void increment(int *p) { (*p)++; }
和直接用两条ensures的区别?
使用
also
语句
语义清晰:
also
子句明确表示附加的后置条件是补充性质的,它不会替换或覆盖之前的ensures
条件,而是在它们的基础上增加额外的保证。条件分组:使用
also
可以清晰地将不同的后置条件分组,使得规范更容易阅读和理解,尤其是当有多个相关的后置条件时。重用性:在某些情况下,如果函数规范被多个不同的上下文重用,
also
可以用于添加特定于某个上下文的额外保证,而不影响规范的其他用途。直接使用两条
ensures
语句
合并条件:当直接使用两条
ensures
语句时,它们在逻辑上是“与”(AND)的关系,即两个条件都必须满足。但是,这种方式没有明确区分哪些条件是基本的,哪些是附加的。缺少语义提示:不使用
also
可能会让阅读规范的人不清楚这些ensures
条件之间的关系,尤其是当规范变得复杂时。可能的重复:在不同的函数规范中重复相同的基本
ensures
条件可能会引入错误,因为每次更改基本保证时,所有重复的地方都需要更新。
axiomatic
:用于定义不变量或全局属性。假设我们有一个简单的数学函数,它计算两个整数的和,并且我们想要声明一个全局的逻辑断言,即这个函数总是返回一个非负数。
/*@ axiomatic AddNonNegative { logic \int result = a + b; axiom result >= 0; } */ int add(int a, int b) { return a + b; }
在这个例子中:
axiomatic AddNonNegative
开始定义一个名为AddNonNegative
的公理组。
logic \int result = a + b;
在公理组内声明了一个逻辑表达式,它表示a
和b
的和。
axiom result >= 0;
声明了一个公理,即a
和b
的和总是非负的。这个
axiomatic
声明了一个全局的逻辑断言,可以在其他规范元素中被引用,以证明或验证add
函数的行为。
assert
:用于断言某个条件必须始终为真。void increment(int *p) { /*@ assert p != NULL; @*/ (*p)++; }
loop invariant
:指定循环中的不变量。假设我们有一个函数,它计算数组中所有元素的和。
int sum_array(int arr[], int n) { int sum = 0; /*@ loop invariant sum == \sum int j; 0 <= j < i have arr[j]; */ for (int i = 0; i < n; i++) { sum += arr[i]; } return sum; }
在这个例子中:
loop invariant sum == \sum int j; 0 <= j < i have arr[j];
指定了一个循环不变量。这个不变量表明,在每次循环迭代开始时,变量sum
的值等于数组arr
中从索引0
到索引i-1
的所有元素的和。\sum是一个累积求和的表达式,用于计算一系列值的总和。在ACSL中,
\sum
通常与泛型迭代器一起使用,来表示对某个范围内的值进行求和。have用于指定
\sum
或其他累积表达式中涉及的值的范围或条件。在与\sum
结合使用时,have
后面通常跟着一个条件表达式,这个条件表达式定义了哪些值被包含在求和中。
loop variant
:指定循环的终止条件,该子句用于指定一个表达式,该表达式在每次循环迭代时递减,以证明循环将在有限的迭代次数后终止。假设我们有一个函数,它计算数组中所有元素的和,我们想要证明这个循环会终止。
int sum_array(int arr[], int n) { int sum = 0; /*@ loop invariant sum == \sum int j; 0 <= j < i have arr[j]; loop variant n - i; */ for (int i = 0; i < n; i++) { sum += arr[i]; } return sum; }
在这个例子中:
loop invariant sum == \sum int j; 0 <= j < i have arr[j];
指定了一个循环不变量,表示在每次循环迭代后,变量sum
包含了数组arr
中从索引0
到当前索引i
的所有元素的和。
loop variant n - i;
指定了一个循环变体,表示每次循环迭代时,n - i
的值都会递减。由于n
是一个常数,而i
在每次迭代中递增,n - i
的值会逐渐减小,直到i
达到n
,此时循环终止。这证明了循环会在n
次迭代后结束,从而保证了循环的终止性。通过使用
loop variant
子句,我们可以向静态分析工具证明循环的终止性。
loop assigns
:指定循环中可能被修改的变量集合。假设我们有一个函数,它将一个字符串中的所有字符转换为大写。
void to_uppercase(char *str, int length) { /*@ loop assigns str[0..length-1]; loop invariant \forall int j; 0 <= j < i ==> str[j] == 'A' + (str[j] - 'a'); */ for (int i = 0; i < length; i++) { str[i] = toupper((unsigned char)str[i]); } }
在这个例子中:
loop assigns str[0..length-1];
指定了循环中被修改的变量,即字符串str
从索引0
到length-1
的所有字符。
loop invariant \forall int j; 0 <= j < i ==> str[j] == 'A' + (str[j] - 'a');
指定了一个循环不变量,表示在每次循环迭代后,对于所有j
(0
到i-1
),字符串str
中的字符已经被转换为大写。
ghost
:用于定义幽灵变量或函数,这些变量或函数在宿主代码中不存在,但用于规范和验证目的。假设我们有一个计数器函数,我们想要确保每次调用这个函数时,一个虚拟的计数器都会增加,并且这个计数器的值永远不会超过某个特定的限制。
void increment_counter() { /*@ ghost int ghost_counter = 0; @*/ ghost_counter++; /*@ assert ghost_counter <= 10; @*/ }
在这个例子中:
ghost int ghost_counter = 0;
声明了一个名为ghost_counter
的幽灵变量,并初始化为0。这个变量在实际的C代码中不存在,不会影响程序的实际执行。
ghost_counter++;
每次调用increment_counter
函数时,幽灵变量ghost_counter
的值会增加。
assert ghost_counter <= 10;
是一个断言,它确保幽灵变量ghost_counter
的值永远不会超过10。如果超过这个值,断言会失败,这可以在静态分析时被检测到。使用
ghost
语句可以让我们在不修改实际代码的情况下,为证明添加额外的逻辑和约束。这在处理复杂的证明或者模拟程序中不存在的行为时非常有用。
\valid :用于表达一个指针或数组是有效的,即它指向的内存是可访问的,并且没有越界。在
assigns
子句中,\valid
可以用来指定函数可能读取或修改的内存位置。\result :用于引用函数返回值的占位符。在后置条件(
ensures
)中,\result
可以用来表达关于函数返回值的属性或约束。\forall:表示“对于所有”,是一个量词,用于表达对于某个集合中的所有元素,某个属性都成立。在
loop invariant
或ensures
子句中,\forall
可以用来表达对所有元素的通用属性。\nothing:用于
assigns
子句中,表示函数不修改任何状态或内存位置。这通常用于纯函数或不产生副作用的函数。\
of
:用于assigns
子句中,指定特定对象的字段或属性。\in:用于表达一个值在某个范围内或属于某个集合。在ACSL中,
\in
通常与\forall
一起使用,指定量词作用的域。\old:用于引用函数执行前的值。在后置条件(
ensures
)中,\old
可以用来比较函数执行前后的状态,例如\result == \old(var)
表示返回值等于变量的旧值。\base_addr:用于表达一个复合字面量的基地址。在ACSL中,
\base_addr
可以用来指定一个复合字面量(如数组或结构体)的起始地址。
Chapter 2 Specification Language
函数合约
后置条件通常需要同时引用函数结果和前置状态中的值。因此,item扩展了以下新结构式:
- \old(e):指的是term或predicate在该函数前的状态对应的值。
- \result:指的是函数的返回值。
举例2.18和2.19:
2.18的isqrt函数功能是取x的算数平方根,对应的合约表示前置条件x必须非负,后置条件是三个子句的合取,在这些子句中,即使函数修改了形式参数x,也没有必要使用\old(x)结构,因为函数调用修改了有效参数的副本,而有效参数保持不变。事实上,x表示isqrt调用的有效参数,它在前状态和后状态下解释的值是一样的。
而2.19的incrstar函数功能是对应指针的内容加一,前置条件需保证p指针是有效的,\valid谓词会在后面介绍,assigns
语句的作用是指定函数或循环中可能被修改的内存位置集合,也就是告诉工具哪些变量的值可能在执行或循环过程中被改变,后置条件就是确保修改过的p指针的内容比原来\old中的内容多1。