ACSL及frama-c学习笔记(二)——ACSL基本语法

        本文主要介绍V1.19的manual,因为我的frama版本是27,每个frama版本都有对应的名字,可按这个名字找对应的manual。

ACSL - Frama-Cicon-default.png?t=O83Ahttps://www.frama-c.com/html/acsl.html

        后续修改:这个啥玩意manual太难读了,可能旧一点的版本会介绍更基础的东西,所以从chapter2函数合约开始转为介绍由一位长期使用 WP 和 ACSL 进行研究工作的业者所撰写的教程,教程内包括基本语法,用法和与frama-c的交叉。

allan-blanchard.fr/publis/frama-c-wp-tutorial-en.pdficon-default.png?t=O83Ahttps://allan-blanchard.fr/publis/frama-c-wp-tutorial-en.pdf

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 来定义这个范围,并在 requiresensures 子句中使用它。

#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.lowerr.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:定义函数的不同行为,每个行为可以有自己的assumesensures

/*@ 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'); 指定了一个循环不变量,表示在每次循环迭代后,对于所有 j0 到 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 invariantensures子句中,\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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值