挖掘代码覆盖率工具中的Bug

挖掘代码覆盖率工具中的Bug

Hunting for bugs in code coverage tools via randomized differential testing

Remarks

Conference: ICSE 2019

Full Paper: yangyibiao.github.io/pa

Summary

  • 针对的问题:挖掘代码覆盖率工具中的错误,保证代码覆盖率工具的可靠性。
  • 目前的研究现状:很少有研究关注代码覆盖工具的可靠性评估,如何选择测试程序,如何在保证相同覆盖率达情况下对测试程序进行剪枝,如何确认bug都是很大的挑战。
  • 提出的创新方案概述:提出了一种随机差分测试方法来搜寻最广泛使用的C代码覆盖工具中的错误。具体来说,通过生成随机输入程序,将不同代码覆盖工具生成的代码覆盖报告进行差分对比,然后将不一致识别为潜在的代码覆盖错误。此外,作者还提出了将大程序自动减小成具有相同属性的小程序的方法,以及当不同的代码覆盖工具不一致时,如何判断哪个代码覆盖工具有Bug。
  • 实验效果:针对广泛使用的应用于GCC编译器的gcov工具,以及LLVM/Clang平台的llvm-cov工具部署大规模实验。该工作在gcov和llvm-cov中分别找到了42和28个已经确认的bug,也说明了我们常用的代码覆盖工具并不像预想的那样可靠,强调了继续提高代码覆盖工具可靠性的必要性。

Introduction

代码覆盖率工具一般指能说明以下两个问题的工具(如今已有gcov,llvm-cov等代码覆盖工具被广泛用于实践):(1) Which code (normally a line) is executed or not? (2) How many times each code is executed? 可靠的代码覆盖工具非常重要,因为它被大量用于促进许多质量保证活动,如软件测试、模糊化和调试等应用场景。

尽管在实践中普遍采用代码覆盖工具,但其仍然存在各种缺陷。下图中的(a)显示了Clang的C代码覆盖工具llvm-cov生成的错误代码覆盖报告。覆盖率报告本来是源代码的注释版本,为了方便在文章中展示,作者将覆盖率报告重新格式化,其中第一列和第二列分别列出行号和执行频率。下图为llvm-cov中的bug#33465,该程序所有语句顺序执行,没有分支,然后llvm-cov工具报告第5行代码没有被覆盖。在EMI编译器测试(V. Le, M. Afshari, and Z. Su, “Compiler validation via equivalence-modulo inputs”)中,会根据代码覆盖率工具的结果,修剪掉测试用例中未执行的语句(如下图中的(b)所示)。当EMI分别执行(a)和(b)两个程序出现不同的结果,则EMI认为这是一个编译器的bug。然而,这明显并不是编译器中的bug。这是由于代码覆盖工具的不可靠导致的编译器测试中的false positive。由于代码覆盖工具提供了整个软件开发过程中所需的基本信息,因此验证代码覆盖的正确性至关重要。不幸的是,据我们所知,很少有人关注代码覆盖工具的可靠性评估。

这项工作是针对代码覆盖率工具测试的第一次尝试。作者设计了一种实用的随机差分测试方法来发现代码覆盖工具中的错误。本文的方法首先利用随机生成器生成的程序来寻找由不同代码覆盖工具生成的代码覆盖报告的不一致性,并将不一致性识别为潜在的代码覆盖错误。其次,由于报告的不一致触发测试程序太多,并且这些测试程序中有大量的无关代码,直接报告这些不一致触发测试对调试几乎没有好处。在报告它们之前,需要减少这些测试程序。然而,减少每一个测试程序通常是非常昂贵的,因此是不现实的。作者发现许多测试程序都会触发相同的覆盖错误。因此,可以过滤掉许多重复的测试程序(本文中的“重复测试程序”表示多个测试程序触发相同的代码覆盖率错误)。

Challenge

  • Challenge 1: Filtering Out Test Programs. 要过滤掉触发相同代码覆盖率错误的潜在测试程序,最直观的方法是使用整个文本计算程序之间的相似性。然而,作者使用Csmith作为随机程序生成器,两个Csmith生成的程序在许多方面不同,因此没有意义上的可比性。此外,使用整个文本计算程序之间的相似性是昂贵的。为了应对这一挑战,只有引发不一致的代码行才能用于计算程序之间的相似性。
  • Challenge 2: Reducing Test Programs. 减少代码覆盖率bug的测试程序,即保留interesting的测试程序,这其实要比减少编译器bug的测试程序复杂得多,因为后者只需要测试已编译可执行文件的行为或编译器的exit code。但是,减少代码覆盖率bug的测试程序需要处理文本代码覆盖率报告并识别不一致。在每次还原迭代之后,我们需要指定要保留的interesting的不一致性。作者设计了一组覆盖率报告的不一致类型作为interesting
  • Challenge 3: Inspecting Coverage Bugs. 在报告错误之前检查哪些代码覆盖工具有错误。实际上,通常是手工完成的。换句话说,开发人员手动检查覆盖率报告,以确定哪些覆盖率工具是错误的。为了减轻手动干预的负担,作者总结了代码覆盖率报告必须遵循的一些规则。根据这些规则,开发了一个工具来检查不一致覆盖率报告的一部分,并自动确定哪些工具有bug。

代码覆盖率工具中Bug的类别主要有:

  • Spurious marking:程序块在运行时未执行,但被错误地标记为由代码覆盖工具执行。
  • Missing Marking:程序块实际上是在运行时执行的,但被代码覆盖工具错误地标记为未执行。
  • Wrong Frequency:错误频率表示程序块在运行时执行了m次,但被代码覆盖工具{(m!=n)∧(m>0)∧(n>0)}错误地标记为执行了n次

下图的图2给出了这样一个例子。在主函数中,第9行和第12行的两个返回语句不能同时执行。因此,其中一个必须被llmv-cov错误标记。在运行时,函数main返回“1”,表示第9行被错误标记。这个例子就属于Spurious Marking,带有虚假标记错误的代码覆盖工具将导致错误标记为已执行的未执行代码。前文图1所示的属于Missing Marking。一个缺少标记的代码覆盖工具将导致一个预定义的覆盖率,无论分配了多少测试时间和资源,这个目标都无法实现。下图的图3显示了一个gcov覆盖率报告,其中第一列列出执行频率,第二列列出行号。可以看到,第11行的代码被错误地标记为执行了两次,但实际上只执行了一次。在许多基于覆盖的软件工程活动中,频率错误的代码覆盖工具可能导致次优决策。


Approach

下图展示了的代码覆盖率验证框架。在下面,将描述这个框架中的关键步骤。特别是,本文将使用gcov和llvm cov作为主题代码覆盖工具来说明寻找代码覆盖bug的方法。

第一步:生成程序作为测试用例

本文的的方法从随机程序生成器开始。生成器随机生成一个大的程序集P,每个程序p∈P将被用作测试程序,用于对两个代码覆盖工具进行差异测试。换言之,每个程序将被馈送到两个代码覆盖工具,以分别获得两个覆盖工具的报告。在本文中,测试程序是指具有相应输入的可编译C程序的集合。作者选择了用Csmith来生成测试程序,是因为:(1)它支持多种C语言特性,能够避免生成行为不明确的程序,从而优于一些其它的随机C程序生成器;(2) 每个生成的程序都是一个单独的文件,输入是自包含的,不需要额外的输入;并且(3)它足够快,可以在几秒钟内生成成千上万行的程序。

第二步:分析代码覆盖率报告

对于生成的程序集P中的每个测试程序p,分别获得覆盖工具t1和t2发出的原始代码覆盖率报告r1和r2。但是,原始覆盖率报告不能直接进行比较,因为它们以不同的格式显示。因此,我们开发了一个解析器,将ri转换为统一格式(1≤i≤2)的uri。具体来说,uri是一个由两个元组组成的序列,包括被监视的程序块和相应的执行频率。给定一个程序p有N行源代码,uri是N个二元组(nj,fj)按nj的值升序排列的序列,1≤j≤N,这里nj是行号,fj是相应的执行频率。如果nj线不是特定覆盖工具的插桩点,则fj被指定为-1。

运行示例:在下图,给出了一个示例来演示解析器在现实世界中是如何工作的。图5(a)显示了gcov为一个程序生成的代码覆盖率报告r1,图5(b)显示了llvm-cov为同一个程序生成的代码覆盖率报告r2。r1和r2是源文件的两个带注释的版本。然而,r1和r2之间有三个区别。首先,他们有不同的插桩位置。一方面,第4、6∼8、12、19和22行被llvm-cov视为仪表位置,而gcov不将它们视作插桩位置。另一方面,gcov将第3行和第18行视为插桩点,llvm-cov则不将其视为插桩点。因此,比较的时候只需要比较gcov和llvm-cov使用的公共插桩点。其次,gcov和llvm-cov对于执行频率有不同的注释格式。在r1中,非检测位点被标记为连字符“-”,但在r2中被标记为空字符串(例如第3行)。此外,未执行的行在r1(例如行15)中记为散列“######”,而在r2(例如行9)中记为“0”。第三,覆盖率统计数据包含在r1中,但在r2中不可用。图5(c)分别列出了语法分析器为gcov和llvm-cov生成的统一覆盖率报告。可见,它们之间共有9个共同的插桩点:第5行、第7行、第9行、第10行、第11行、第13行、第14行、第15行、第20行和第21行。

第三步:比较统一化后的覆盖率报告

在得到统一的覆盖率报告ur1和ur2之后,我们使用工具比较器来确定它们是否一致。否则,相应的p和相关的覆盖率报告将被添加到名为IPS(不一致触发测试程序集)的集合中。在比较统一覆盖率报告时,比较器只考虑不同覆盖率工具中公共插桩点的执行频率。在ur1和ur2的比较中,可能会出现以下类型的情况:

  • Type A: one line code is marked as executed by t1 but as unexecuted by t2;
  • Type B: one line code is marked as unexecuted by t1 but as executed by t2;
  • Type C: one line code is marked as executed k times by t1 and as executed l times by t2 while k = l.

因此,不一致的统一报告可以分为七类(C001:C类;C010:B类;C100:A类;C011:B+C类;C101:A+C类;C110:A+B类;C111:A+B+C类)。考虑运行示例中的两个统一的覆盖报告,比较器将比较公共9个插桩点的执行频率,以确定ur1和ur2是否一致。可以看出,在gcov和llvm cov的统一覆盖报告中,存在四个不一致的执行频率:第5行的C类、第13行的C类、第9行的A类和第15行的B类。因此,ur1和ur2的不一致类别被发现为C111。换句话说,测试程序p引入的不一致性属于C111类。

第四步:过滤测试程序

直观地说,使用更大的测试程序集P有更高的机会发现代码覆盖错误。因此,在实际应用中,我们更倾向于生成大量的测试程序,进而导致大量的不一致触发测试程序。这将导致以下两个问题。一方面,检查所有触发测试程序的不一致性可能是不现实的,因为降低成本非常昂贵,并且检查资源通常是有限的。另一方面,我们观察到许多测试程序触发相同的代码覆盖率错误。因此,我们可以过滤掉重复的测试程序(基于报告中不一致行数的相似性)。

第五步:缩小测试程序

由于测试程序可能具有较大的代码大小,因此确定相应的覆盖率oracle既费时又容易出错。此外,如果大型测试程序作为POC提交给开发者,开发人员也很难在代码覆盖工具中确定错误的位置。为了解决这个问题,我们使用Reducer通过删除与不一致性无关的代码来减少IPS中的每个不一致触发测试程序。

第六步:检查最终(简化后的)的测试程序集

随着测试程序的减少,我们需要在报告之前检查哪些代码覆盖工具有错误。实际上,通常是手工完成的。为了减轻人工干预的负担,我们总结了代码覆盖率报告必须遵守的以下规则:

尽管使用了上述规则,但仍然存在一些无法自动检查不一致的覆盖报告的情况,那么就人工检查这些覆盖率报告。

第七步:将代码覆盖率bug报告给开发者

对于RS-IPS中的每个测试程序,此步骤只是为相应的错误工具生成bug报告。bug报告主要由简化的测试程序和受影响的版本组成。如果一个测试程序触发多个bug,将生成多个单独的bug报告。

Evaluation

作者实现了一款工具原型C2V(Code Coverage Validation),并在针对两款被广泛使用的代码覆盖工具gcov和llvm-cov部署实验。

该工作还在gcov和llvm-cov中分别找到了42和28个经确认过的bug,也说明了我们常用的代码覆盖工具并不像预想的那样可靠,强调了继续提高代码覆盖工具可靠性的必要性。

编辑于 04-15

文章被以下专栏收录