本文学习自《嵌入式软件动态运行时错误的检测》,主要是了解一下polyspace,工作过程中目前没有涉及到。
背景
和桌面系统不同,对于嵌入式软件系统,软件测试主要是发现以下类型的错误:
- 功能错误 —— 主要借助项目需求文档,编写对应的测试用力进行测试与验证工作
- 性能错误 —— 一般要借助硬件级别的工作,衡量软件的性能是否达到要求
- 运行时错误 —— 软件在动态运行时出现的错误,是所有的软件错误中最具风险的
运行时错误
运行时错误由ANSI C定义,是指那些能导致预定义之外的不正确结果或处理器停机的错误,其后果包括:
- 处理器停机
- 数据崩溃
- 安全保密受到破坏
典型的运行时错误有以下几种:
- 访问未初始化的变量
- 对空指针和越界指针的引用(内存冲突)
- 对超界数组的访问
- 非法类型转换(long to short,float to integer)
- 非法的算数运算(除零错误,负数开方,变量的上下溢出)
- 非法的移位运算
- 死代码
- 多任务重共享变量的访问冲突
如何检测运行时错误
传统的软件测试技术一般分为静态测试和动态测试,这两种测试方法在检测软件动态运行时错误方面有较多限制:
对于静态技术:
- 不分析控制代码的动态行为(loops、if、switch、function calls)
- 不分析变量之间的关系
对于动态技术 - 需要依赖测试用例
- 需要实际的运行环境
- 只能看到错误结果,错误原因需要自己检查
可见,静态测试技术可以检查软件代码的编程规范,分析程序的静态结构,对软件的质量进行度量。借助静态测试技术,可以使代码规范,结构清晰,但是不能有效地检查出只有动态运行才会出现的错误。
动态测试技术的一般步骤如下:
测试计划 -> 测试用例 -> 测试执行 -> 发现并提交BUG
这种方法一定程度上可以发现部分运行时错误,即测试用例所能覆盖到的错误,但是由于无法穷尽所有的输入,所以依赖于测试用例的测试最终只能保证在测试用例输入下不会导致运行时错误,无法保证其他输入情况下也可以正常工作。
既然上面的方法有缺陷,就需要借助新的方法与工具来实现运行时错误的检测 —— Polyspace
PolySpace的语义分析
语义分析技术,依靠大量的数学定理提供的规则去分析软件的动态行为,这种方法没有使用简单的穷举法,但却可以在更普通的模式下表达程序的状态。
举个例子:
一个程序中使用了两个变量 x 和 y,对以下语句进行运行时错误检查:
x = x / (x-y);
- step one:列举该语句可能存在的所有运行时错误:
- x 和 y 可能没有初始化
- x - y 可能会溢出
- x 和 y 可能会相等,进而导致除数为0
- x / (x-y) 可能溢出
- step two:为了更好地理解语义分析,我们在二维坐标系中表示x和y的取值,如下:
其中,红线表示会导致除数为0的x和y的集合(即x = y) - step three:根据上面的图,我们如何去判断是否会出现除数为零的状态?
语义分析的方法是建立自己的规则来熟练处理所有的状态,对程序进行抽象,怎么做?举个例子(间隔分析)
根据坐标系中x和y的坐标,得到x和y的最小值和最大值,画一个相应的矩形,很显然,矩形和红线的交集就是我们关系的集合,换句话说,如果交集为空,就说明除数不可能为0.
-step four:怎么高效地进行运行时错误的检测,其实第3步体现了从程序中得到得到一个简单的抽象(矩形),这个抽象采用的是间隔分析的方法,这个矩形不是太好(原因:包括了很多不实际的x和y值),运行时错误的检测结果会包含大量的警告信息,已经不适合在此基础上做实际的分析了。
问题来了:怎么建立一个合适的形状?
语义分析技术能够根据自己的规则,建立非常精确的形状,基于变量之间的关系,程序的控制结构(if-else、for、switch等),内部过程之间的关系(函数的调用),多任务分析等,进行运行时错误检测。
这里不涉及太多具体的规则(主要是不懂),经过一堆规则之后,上面的例子可以重新画出一个比较好的形状
PolySpace的优势?
以代码覆盖率测试为例,这里我们要求代码覆盖率达到100%
static void Recursion(int* depth)
/* if depth<0, recursion will lead to division by zero */
{
float advance;
*depth = *depth + 1;
advance = 1.0/(float)(*depth - 6); /* potential division by zero */
}
我们仅仅需要一个测试用例*depth = 10
即可使代码覆盖率达到100%,但是却没有发现最简单的Bug,例如*depth = 5 会导致除数为0这样的致命的错误。
覆盖率测试可能发现不了一些与数据相关的错误
覆盖率测试不可能查出程序中因遗漏路径而出错
当然,实际的代码可能比上述程序简单,假设GlobalFlag是一个被多任务使用的全局变量。
static void Recursion(int* depth)
/* if depth<0, recursion will lead to division by zero */
{
float advance;
*depth = *depth + 1;
advance = 1.0/(float)(*depth - GlobalFlag); /* potential division by zero */
}
*depth = 10这一个测试用例可能也会让代码覆盖达到100%,但这个100%能保证下面代码中的除数不为0吗?显然不能。
从另一个角度来看一下覆盖的概念
代码覆盖率不能仅考虑程序逻辑、语句分支的覆盖,还需要考虑数据输入的覆盖度
以int* depth 为例,取*depth = 10,代码覆盖率是达到了,但是对应的输入数据仅覆盖掉1/65535。
实际测试也不可能针对每个变量进行输入数据的全覆盖,所以传统的代码覆盖率检查很容易漏掉难以察觉的致命问题。
polyspace采用的是类似穷举的语义分析方法,可以将所有的可能情况都检查一遍,只要有一个输入存在问题,polyspace结果中就有相应的提示,这样的话,其实从代码覆盖率和数据输入的覆盖度两个方面都保证了100%的覆盖。