代码改变世界

KLEE: Unassisted and Automatic Generation of High-Coverage Tests for Complex Systems Programs

2018-05-30 18:45  huhee  阅读(1032)  评论(0编辑  收藏  举报

题目:KLEE: Unassisted and Automatic Generation of High-Coverage Tests for Complex Systems Programs
作者:Cristian Cadar, Daniel Dunbar, Dawson Engler ∗
单位:Stanford University
出版:USENIX Symposium on Operating Systems Design and Implementation (OSDI 2008)
December 8-10, 2008, San Diego, CA, USA

背景:
 很多类型的错误(比如功能正确性错误),如果不执行部分代码,很难发现它们。这些测试的重要性以及随机、人工方法的困难和不好的表现使得最近关于使用符号执行自动生成测试用例的工作多了很多。

贡献:

    开发了新的符号执行工具——KLEE,可以更加深入地对一系列的应用进行测试,吸收了先前工具EXE的多年经验。KLEE使用了很多的约束求解的优化,程序的状态表示更加简洁,使用启发式搜索来获得高的代码覆盖率。此外,KLEE使用了一个简单且直接的方式来处理外部环境。这些特征使得KLEE的表现大幅提高,并且可以非常好地测试大量的系统敏感性程序。
    KLEE在不同的真实、复杂和环境敏感的程序集合上自动生成的测试可以得到很高的覆盖率。KLEE被用来测试GUN COREUTILS(version6.10)上的89个程序以及其他的UNIX工具套件:BUSYBOX和MINIX。最后对HISTAR操作系统内核进行测试,与应用代码做对比。


架构
KLEE是对EXE的一个完全的重新设计。KLEE的功能可以看作是符号化过程操作系统和解释器的混合体。每一个符号化过程有一个注册文件、栈、堆、程序计数器和路径条件。
为了避免和Unix进程的混淆,将KLEE的符号化过程表示称为一个state。程序被编译为LLVM汇编语言(一种类似RISC的虚拟指令集)。KLEE直接解释这个指令集,将指令无近似地映射为约束(比如位精度)。

1.基本架构
在任何时间,KLEE可以执行大量的状态。KLEE的核心是一个翻译循环:选择一个state去执行,然后符号化地执行指令的下一个状态。这个循环的终止条件是没有其他的状态或者达到了用户自定义的时间超时。
与正常进程不同的是,状态的存储位置(寄存器、堆栈和堆对象)存储的是表达式(树),而不是原始数据值。一个表达式的叶子是符号变量或常量,内部节点是来自LLVM汇编语言的操作(比如四则运算、位操作、比较和内存访问)。常量表达式的存储位置称为具体的存储位置。
条件分支取一个布尔表达式(分支条件)并根据状态为真或假来改变状态的指针。KLEE查询约束求解器,来确定当前路径的分支条件是可证明为真或可证明为假;如果可证明真假,指令指针更新到适当的位置。否则,两分支都是可能的:KLEE克隆状态,可以探索两条路径,更新每条路径的指令指针和路径条件。
潜在的危险操作隐式生成分支,检查是否存在任何可能导致错误的输入值。例如,一个除法指令生成一个分支来检查一个零因子。这些分支与正常分支工作相同。因此,即使在检查(check)成功(即检查到错误)时,仍会继续在false路径上进行,这将检查的否定作为约束(例如,使除数不是零)。如果检测(detect)到错误,KLEE产生测试用例来触发错误并终止状态。
与其他危险操作一样,Load和store指令生成检查:在这种情况下,检查地址是否在有效内存对象的边界内。但是,加载和存储操作会带来额外的复杂性。检查代码使用的内存最直接的表示形式是平面字节数组。在这种情况下,load和store会简单地映射到数组上,分别读写表达式。不幸的是,我们的约束求解器STP不可能求解所有的约束。因此,KLEE将被检测代码的每个内存对象映射到一个特定的STP数组中(从某种意义上说,将平面地址空间映射为分段地址空间。)这种表示方法很大程度地提高了性能,因为这使得STP忽略了所有的没被给出的表达式引用的数组。
许多操作(如绑定检查或写对象级复制)需要特定于对象的信息。如果一个指针可以引用许多对象,这些操作就很难执行。为简单起见,KLEE解决这个问题如下:如果解引用指针P引用了N个对象,KLEE将当前的状态克隆N次。在每个状态中,它将p限制在其各自对象的边界内,然后执行适当的读或写操作。虽然这种方法对于指向大量指针集的指针来说花费很大,但是在我们已测试的程序中,大部分只使用指向一个对象的符号指针,对于这种情况KLEE做了很好的优化。

2.紧凑的状态表示(Compact State Representation)
路径爆炸问题:状态的数目增加很快,即使是小程序也可能在解释的前几分钟内生成成千上万的并发状态。
状态表示的内部化极大地增加了可以同时探索的状态的数量,这是通过降低每个状态成本(在对象层级应用复制写入,很大程度地减少了了每个状态的内存需求)和允许在对象(而不是页面)级别共享内存实现的。

3.查询优化
约束求解的成本几乎占主导地位的,由此KLEE生成了一个NP完全逻辑复杂查询。
KLEE通过简化表达式的技巧,在到达STP之前,尽量消除查询(没有查询是最快的查询)。简化查询可以更快地解决问题,减少内存消耗,并提高查询缓存的命中率。主要的查询优化是:
表达式重用
约束集简化
隐含value的具体化
约束独立
反例缓存

4.状态调度
(1)KLEE通过在每个指令下选择一个状态,交互地运行两种启发式搜索——随机路径选择和覆盖优化搜索。
随机路径选择维护一个二叉树,记录所有活动状态之后的程序路径,即树的叶子是当前状态,内部节点是执行分叉的地方。通过从根遍历该树并随机选择在分支点上追踪的路径来选择状态。因此,当一个分支点到达时,每个子树的状态集被选中的概率相等,不考虑其子树的大小。
这个策略有两个重要属性:
倾向于树的高层分支的状态。这些状态对其符号输入的约束较少,所以有更大的自由去接触未覆盖的代码。
避免饥饿。
覆盖优化搜索试图选择可能覆盖新代码的状态。它使用启发式计算每个状态的权重,然后根据这些权重随机选择一个状态。目前,这些启发式算法考虑到未覆盖指令的最小距离、状态的调用堆栈、以及该状态最近是否覆盖新代码。
KLEE循环使用每个策略。虽然这会增加实现高覆盖率策略的选择时间,但它可以防止个别策略被卡住的情况。而且,由于策略从同一状态池中选择,交错使用它们可以提高整体效能。

    KLEE为避免经常执行耗费很大的状态的指令占据大部分的执行时间,通过定义每个状态的运行时间片,限制每个状态的执行时间。


实验:

1.分类:
1.进行密集运行,既发现错误,并获得高覆盖率。 (COREUTILS, HISTAR, and 75 BUSYBOX utilities)
2.快速运行许多应用程序来查找bug。 (an additional 207 BUSYBOX utilities and 77 MINIX utilities),
3.核查替代程序以发现更深层次的正确的bug。(67 BUSYBOX utilities vs. the equivalent 67 in COREUTILS)
(总的来说,在超过452个项目上运行了KLEE,包430k行代码)
2.实验结果:
1.KLEE在一系列复杂的程序上获得了高覆盖率。自动生成的测试覆盖了84.5%的COREUTILS代码行以及90.5%的BUSYBOX 代码(忽略库代码)。平均来讲,这些测试在每个工具上都达到了90%的命中(中位数:94%),在16个 Coreutils工具和31个busybox工具上实现完美的覆盖率100%。
2.KLEE可以获得比集中、持续的手工工作更大的代码覆盖率。KLEE只用了大约89小时,产生的Coreutils代码行覆盖率,就击败了开发者自己15年来建立的测试套件,并高出了16.8%!
3.KLEE在不变的应用上可以达到很高的覆盖率结果。唯一的例外是Coreutils的sort,需要一个单一的编辑来收缩大的缓冲区,这带来了约束求解器的问题。
4.KLEE在大量测试的代码上发现了重要的错误。它发现了Coreutils的十个致命错误(其中3个历时15年未被检测出来),这比2006、 2007和2008三年发现的奔溃bug的总和还要多。进一步又在busybox找到了24个bug,在MINIX 中找到21个错误,并在HISTAR中发现一个安全漏洞。共计56个严重的错误。
5.测试用例可以运行在代码的原始版本上(例如,用gcc编译),大大简化了调试和错误报告。例如,所有的Coreutils的bug都在两天内被验证和修复,KLEE生成的各个版本的测试被包含在标准的回归套件中。
6.KLEE不局限于低层次的编程错误:当用于测试busybox和GNU Coreutils工具时,它会自动发现功能正确性bug。
7.KLEE也可以应用于非应用程序代码。当运行在 HISTAR的内核,它在有磁盘情况下取得了76.4%的平均行覆盖率,无磁盘时是67.1%。而且KLEE还发现了一个严重的安全漏洞
未来工作:
长期目标是对任意程序,常规地获得最低90%的代码覆盖率。