美国火星车形式化验证案例

在这里插入图片描述
Figure 4. Life cycle of a code comment; orange arrow indicates where the developer
disagrees with a code change but is overruled in the final review.
通过分析确定的几个漏洞可以在任务发射之前从代码中消除,有效地有助于减少飞行意外的风险。使用我们开发的工具,软件模型检查的基本过程可以用一个小例子来说明。(因为NASA的规则阻止我们发布探测器的实际飞行代码,所以我们在这个例子中使用了等效的公共领域代码。)
在这里插入图片描述

手动证明并发算法在所有可能的执行场景下是正确的是不合理的。我们以Detlefs等人中提出的双侧队列的非阻塞算法和四页对正确性证明的总结算法为例。在它发表的几年后,人们试图用一个定理证明器来正式证明它,作为硕士论文项目的一部分。形式化结果表明,原始的证明和算法都存在缺陷。用定理证明器证明对算法的修正是正确的。据报道,原始算法和修正版本的每一次证明尝试都花了几个月的时间。

Lamport后来在+CAL中形式化了原始算法,表明通过模型检查器可以更快地发现缺陷。Lamport指出,TLA+模型检查器的证明可以在不到两天内完成,其中大部分需要用模型检查器支持的语言定义原始算法的正式模型。如图所示,模型提取器也可以帮助避免手动构建正式模型的需要,允许我们在几分钟内而不是几天内对多线程代码片段执行这些类型的验证。我们使用detlef的原始算法来展示这种验证方法是如何工作的。有了它,要查找算法实现中的缺陷,只需要输入几行文本并执行一个命令。

Figure 6. Semantics of the DCAS instruction. 
boolean DCAS (val *addr1, val *addr2,
 val old1, val old2,
 val new1, val new2)
{
 atomically {
 if (*addr1 == old1 && *addr2 == old2)
 { *addr1 = new1;
 *addr2 = new2;
 return true;
 } else
 { return false;
 } }
}
Figure 7. C code for pushRight and popRight routines. 
 1 Node *Dummy, *LH, *RH;
 2 
 3 val
 4 pushRight(val v)
 5 { Node *nd, *rh, *lh, *rhR;
 6 
 7 nd = (Node *) spin_malloc(sizeof(Node));
 8 
 9 if (!nd) return FULL;
10 
11 nd->R = Dummy;
12 nd->V = v;
13 
14 while (true) 
15 { rh = RH;
16 rhR = rh->R;
17 if (rhR == rh)
18 { nd->L = Dummy;
19 lh = LH;
20 if (DCAS(&RH,&LH,rh,lh,nd,nd))
21 return OKAY;
22 } else
23 { nd->L = rh;
24 if (DCAS(&RH,&rh->R,rh,rhR,nd,nd))
25 return OKAY;
26 } }
27 }
28 
29 val
30 popRight(void)
31 { Node *rh, *lh, *rhL;
32 val result;
33 
34 while (true)
35 { rh = RH;
36 lh = LH;
37 
38 if (rh->R == rh)
39 return EMPTY;
40 
41 if (rh == lh)
42 { if (DCAS(&RH,&LH,rh,lh,Dummy,Dummy))
43 return rh->V;
44 } else
45 { rhL = rh->L;
46 if (DCAS(&RH,&rh->L,rh,rhL,rhL,rh))
47 { result = rh->V;
48 rh->R = Dummy;
49 rh->V = null;
50 return result;
51 } } }
52 }

该算法使用原子双字比较交换或DCAS指令;图6给出了detlef中定义的该指令的语义。2图7复制了Detlefs中的两个C例程,用于在队列的右侧添加一个元素,并从同一侧删除一个元素。从队列左侧添加或删除元素的例程是对称的。所使用的节点结构有三个字段:左指针L、右指针R和整数值v。

为了验证代码,我们首先定义了一个简单的测试驱动程序,它通过添加和删除元素来练习代码(参见图8)。为简单起见,本示例只使用pushRight()和popRight()例程。

Figure 8. C code for a sample test driver. 
53 void
54 sample_reader(void)
55 { int i, rv;
56 
57 while (!RH)
58 { /* wait */
59 }
60 
61 for (i = 0; i < 10; i++)
62 { rv = popRight();
63 if (rv != EMPTY)
64 { assert(rv == i);
65 } else
66 { i--;
67 } }
68 }
69 
70 void
71 sample_writer(void)
72 { int i, v;
73 
74 initialize();
75 
76 for (i = 0; i < 10; i++)
77 { v = pushRight(i);
78 if (v != OKAY)
79 { i--;
80 } }
81 }

在图8中的示例测试驱动程序中,写入器在第74行上初始化队列,并且读取器将等待,直到在第57-59行上完成此步骤。读取器在第64行包含一个断言,以验证写入器发送的值以正确的顺序接收,没有遗漏。

我们可以使用不同的阅读器和作者的线程来执行测试,尽管这些测试本身并不能确定算法的正确性。模型检查器被设计为更严格地执行这种类型的检查。如果线程执行中有任何可能的交叉执行,并可能触发断言失败,则保证模型检查器能够找到它。为了使用模型检查器,我们定义了一个小的配置文件,它标识了我们感兴趣的代码部分这个配置文件允许我们为我们想要验证的代码的相关部分,并将它们放入可执行系统中,定义执行上下文,然后进行分析。

图9显示了验证此应用程序所需的完整配置文件。前四行标识了源文件dcas中的四个函数。我们有兴趣提取作为已检测的函数调用。接下来的两行将示例读取器和示例写器标识为将调用这些函数的活动线程。配置文件中的最后三行定义了所需的头文件dcas.h,其中包含数据结构节点的定义和源文件的名称(dcas.c)的附加例程,包括定义DCAS指令语义的函数的C编码(如图6所示)。

Figure 9. Modex configuration file. 
%X -e pushRight
%X -e popRight
%X -e initialize
%X -e dcas_malloc
%X -a sample_reader
%X -a sample_writer
%D
#include “dcas.h”
%O dcas.c

现在可以使用模型提取工具Modex和模型检查工具ModexSpin(参见图10)。该命令大约需要12秒的实时执行,其中验证本身只需要0.02秒。其余的运行时由模型提取器从源代码生成验证模型,由Spin将该模型转换为优化的C代码,最后由C编译器生成执行验证的可执行文件。这些步骤都不需要进一步的用户交互。

对错误路径的回放显示了一个可能导致断言违反的竞争条件,因此显示了算法是错误的(参见图11、12和13)。由写入器进程执行的语句用W进行标记,由阅读器进程用r执行的语句进行标记。首先考虑图11。在sample_ writer例程的initial(图8中的初始调用第74行)之后,写入器启动对pushright第77行的第一次调用,值为0。然后通过在pushright程序中执行第7到第19行来存储该值。

Figure 10. Verification steps.
$ time modex -run dcas.c
MODEX Version 2.0 - 2 September 2011
c_code line 111 precondition false:
 (Psample_reader->rv==Psample_reader->i)
wrote model.trail
...
pan: elapsed time 0.02 seconds
7.69 user 4.02 system 0:12.04 elapsed 97% CPU
$
Figure 11. Part 1, partial execution of pushRight by the test writer. 
74 W: initialize()
76 W: i = 0
76 W: (i<10)
77 W: # v = pushRight(i) ::
 7 W: nd = (Node *) spin_malloc(sizeof(Node));
 9 W: !(!nd)
11 W: nd->R = Dummy;
12 W: nd->V = v;
14 W: (true)
15 W: rh = RH;
16 W: rhR = rh->R;
17 W: (rhR == rh)
18 W: nd->L = Dummy;
19 W: lh = LH;
Figure 12. Part 2, call to popRight by the test reader. 
57 R: !(!RH)
61 R: i = 0
61 R: (i < 10)
62 R: # rv = popRight() ::
34 R: (true)
35 R: rh = RH;
36 R: lh = LH;
38 R: !(rh->R == rh)
41 R: (rh == lh)
42 R: DCAS(&(RH),&(LH),rh,lh,Dummy,Dummy)
43 R: return rh->V;
Figure 13. Part 3, second call to popRight by the reader, with the writer still stalled in its 
first call to pushRight, leading to the assertion violation. 
62 R: rv = popRight(i) # rv is 0
63 R: (rv != EMPTY) # true
64 R: assert(rv == i) # true
61 R: i++; # i is now 1
61 R: (i<10) # true
62 R: rv = popRight() # rv is again 0
63 R: (rv != EMPTY) # true
64 R: assert(rv == i) # false

执行pushRight的下一个语句现在是在DCAS上的调用来完成更新,但该调用被延迟。同时,sample_reader可以继续调用popRight来轮询队列的新元素(见图12)。第一次调用(图8中的第62行)成功并检索存储的值0。图12中的其余步骤说明了该调用的popRight例程的执行。此调用应该不会成功,因为由图11中的写入者发起的pushRight调用尚未完成其更新。但陷阱已经设置。在增加i的值之后,samapak_reader线程现在转移到下一个调用。对popRight的第二次调用完成之前的方式,并再次返回值0,导致失败(参见图13)。

这里使用的模型提取方法的定义方式是这样的,它允许在基本应用程序中使用非常简单的仪器类型。模型提取器总是保留应用程序的原始控制流。但是,它也支持在配置文件中定义更高级的抽象函数(类似于图9中的抽象函数),这可以用于减少提取的模型的复杂性。默认的转换规则定义了从源代码到模型的语句的一对一映射,允许直接验证一组惊人的多线程C程序和算法。

MSL任务广泛使用这种自动化能力来验证关键的多线程算法,直接使用它们在c中的实现。对于更大的子系统,我们还以更传统的方式手工构建了自旋验证模型并对其进行分析。

其中最大的MSL子系统是一个关键的数据管理模块通过大约45,000行C实现。在与模块设计者的密切合作下,该子系统的设计被手动转换为一个大约1,600行的自旋验证模型。在大多数情况下,模型检查运行成功地识别出了软件中可以弥补的微妙并发缺陷的存在。特别是对于文件系统软件,模型检查运行成为我们回归“测试”的常规部分,在代码的每次更改之后执行,经常让我们惊讶,它很容易识别新提交的编码错误。
在这里插入图片描述

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值