当前位置:文档之家› 嵌入式软件动态运行时错误的检测

嵌入式软件动态运行时错误的检测

嵌入式软件动态运行时错误的检测刘艳会1 概述嵌入式软件是世界上重要应用软件的核心,目前已经广泛应用于国防、航空航天、医疗等重要行业中,确保它的稳定可靠是极为重要的任务。

随着当前应用系统规模的增大和复杂度的增加,嵌入式软件测试的成本也直线上升,同时也突出了当前所用测试工具和测试方法的局限性。

和桌面系统不同,对于嵌入式软件系统,软件测试主要是发现以下类型的错误:功能错误主要依靠测试人员依据项目需求说明,编写功能测试用例并运行测试用例,从而验证软件的功能性能错误一般需要放到真实的环境中,借助于硬件级别的工具,来衡量软件的性能是否达到要求运行时错误(Run-Time Error)以前没有有效的检测技术,一般不会专门做运行时错误的检查对于嵌入式系统软件来说,这些错误中,运行时错误是最难发现、又最具风险的错误。

欧洲航天局阿里亚纳501号火箭在升空后不到20秒就发生爆炸,其原因就是因为控制飞行姿态的一段代码蕴含有一个变量溢出的运行错误,发生了溢出的变量控制着火箭急速转向而过载。

那么到底什么是软件运行时错误呢?运行时错误都包括什么类型呢?1 什么是软件运行时错误?(Run-time Errors)运行时错误(Run-time Errors)就是软件在动态运行时出现的错误,是所有的软件错误中最具风险的。

相信熟悉Windows应用系统的人来说,一定经历过以下的情形:图1.1 Windows 2000操作系统上发生运行时错误的现象图1.2 Windows XP操作系统上发生运行时错误的现象图1.3 UNIX操作系统上发生运行时错误的现象这种情况下,不管我们做什么选择,应用程序都会退出。

可能对于一般的软件来说,出现这样的错误没关系,但对于航空航天、汽车以及医疗设备等安全级别要求非常高的系统来说,一旦出现这样的运行错误,损失就是不可估量的。

运行时错误由ANSI C定义,是指那些能导致预定义之外的不正确结果或者处理器停机的错误,它是所有的软件错误中最具风险的,其后果包括:处理器停机,数据崩溃、安全保密受到破坏等。

典型的运行时错误有以下几种:・企图读未初始的变量・对空指针和越界指针的引用・对超界数组的访问・非法类型转换(long to short, float to integer)・非法的算数运算(例,除零错误,负数开方)・整数和浮点数的上溢出/下溢出・多线程应用中未保护数据的访问冲突・不可达到的代码运行时错误属于潜在的威胁,广泛的存在各种软件中。

根据Berkeley大学与 IBM Watson的研究报道指出所有IBM大型软件的漏洞中,30%-40%是运行时错误引起的。

既然运行时错误如此广泛存在,那么我们应该怎样检测运行时错误呢?2 普通软件测试技术的限制传统的软件测试技术一般分成静态测试和动态测试,这两种测试方法在检测软件动态运行时错误方面有着很多的限制,如图2。

图2 普通测试技术的限制静态测试技术可以检查软件代码的编程规范,分析程序的静态结构,对软件的质量进行度量。

借助于静态测试技术,可以使代码更加规范,结构更加清晰,但由于静态测试技术不分析代码的动态行为,不分析各个变量之间的关系,因此普通的静态测试技术不能有效的检查出只有动态运行才会出现的错误,即运行时错误。

动态测试技术需要将被测的程序运行起来,最好能放到实际的软硬件环境中。

动态测试技术主要有以下的特点:z不完全:测试的步骤一般是:测试计划——测试用例——测试执行——发现并提交BUG。

这种方法只能发现一部分运行时错误,即测试用例所能覆盖到的错误,但是完全的测试是不可能的(软件测试的原则之一),即不可能穷尽所有的输入,所以依赖于测试用例的测试最终只能保证测试过的输入不会导致运行时错误,不敢保证其他大部分的输入也能正常工作。

z效率低:动态测试技术能发现一部分运行时错误,但它发现的只是现象,类似于图1这样的现象,而不是问题的根源。

测试人员提交BUG后,开发人员还需要重现BUG,然后使用传统的调试工具来定位问题所在。

对于一般的错误,定位并修复一个错误大约需要10个小时,而对于偶尔死机这样的错误,则需要更多的时间去调试。

3 PolySpace的语义分析技术PolySpace使用的是目前最新的语义分析技术,它依靠大量的数学定理提供的规则去分析软件的动态行为。

语义分析技术没有使用简单的穷举法,却有能力在更普通的模式下去表达程序的状态,还能提供规则巧妙的去处理它。

在以前的软硬件环境下,这个问题会非常复杂,很难去解决。

随着计算机处理能力的不断增强,在当前的硬件条件下,语义分析技术已经完全可以高效的实现。

当应用在运行时错误的检测时,语义分析技术会对所有危险的操作,执行一个详细的分析,在程序动态运行之前,最早在编码阶段,就能够发现其中的运行时错误。

举例说明,一个程序P,使用了两个变量:x和y。

检查语句X = X / ( X-Y )的运行时错误。

z第1步:为了保证穷尽,先列举该语句可能存在的所有运行时错误。

X和Y可能没有初始化X – Y 可能会溢出X和Y可能会相等,从而导致除数为0X / ( X – Y )可能会溢出下面以第3个为例,详细介绍一下语义分析如何检查除数为0z第2步:为了更好的理解语义分析,可以在二维的坐标系中表示X和Y所有可能的取值。

红线表示能够导致除数为0的X和Y的集合。

图3.1 建立二维的坐标系z第3步:根据这个图,我们如何去判断是否会出现除数为零的状态?最直观的方法就是穷举X和Y的每一个状态,检查它是否在红线上,这个活动称为“测试”,并且我们能够很快了解该原理的局限性:因为在实际的程序里肯定不只两个变量,所以将会有无数个状态。

如果要穷举所有的状态,花费的时间将会无限长穷举法不能查出一些运行时错误,包括读取未初始化的变量语义分析的方法是建立自己的规则来熟练处理所有的状态。

如何对程序进行抽象,可以检测程序的特性呢。

如下是一个简单的例子:间隔分析:根据坐标系中X,Y的坐标,得到X和Y的最小值和最大值,画出一个相应的矩形图。

这个矩形有以下特性:它包括X和Y所有可能的取值,用四个数据表示(X的最小值,X的最大值,Y的最小值,Y的最大值)。

什么特性是最让人关心的?显然矩形和红线的交集是我们最关心的,就是说,如果交集是空集的话,就说明除数不可能为0。

图3.2 矩形中包含了X和Y的所有可能的取值,通过X和Y的最大和最小值绘制。

这种类型的抽象,也叫做间隔分析。

编译器、链接器和一般的静态分析工具已经在使用这种技术。

z第4步:根据语义分析的概念,我们怎样才能高效的用来进行运行时错误的检测呢?第3步表现了一个从程序中得到的非常简单的抽象(一个矩形),该抽象采用的是间隔分析的技术,目前大多数的编译器、链接器和一般的静态分析工具都已经在使用这种技术。

这种方法的问题是抽象出来的矩形不是一个很好的形状,因为它包含了海量的不实际的X、Y的值,运行时错误的检测结果包含了大量的警告信息,不适合实际的分析。

语义分析技术能够依据自己的规则,建立非常精确的形状,包括网格或者多个多边形,基于变量(X,Y)之间的关系,程序的控制结构(if-then-else, for, while loops, switch),内部过程之间的关系(函数调用),多任务分析,进行运行时错误的检测。

语义分析的技术原理不仅仅是计算数据类型和常量的值,象编译器和一般的静态分析工具那样,语义分析包括很多方面。

它起源于这些包括在程序里面的每一个操作的相关的语义和操作数据的关系,并以此作为基础去详细审查源代码和细微的运行时错误,举例如下:传递给函数的参数不再是一个变量或者常量,而是一个用来约束函数内部局部变量的集合在多任务的程序中,全部共享变量的值随时都会改变,除非使用了保护机制 变量不仅仅是有一定取值范围的类型,还是用一套包括控制流关系去表示的方程式最后,由于使用了变量的方程式,语义分析能够解决运行时错误的检测图3.3 语义分析技术可以验证控制结构、内部过程和多任务的分析,从而可以测试程序的动态属性,高效的检测运行时错误。

4 例子以下的例子是由语义分析测试的结果,标成红色的代码表示错误。

以下的例子请仔细的思考,这里不再做详细的说明。

4.1 控制结构分析:for循环结束后指针访问越界借助于语义分析,以上指针访问越界(Out of Bounds pointer de-referencing)的错误被自动检测到,而其他的操作都是安全没有错误的。

如果不修复,该错误将导致内存冲突,需要大量的调试时间。

程序员对这种错误非常熟悉。

语义分析是唯一能够在编译时就能发现这些错误的解决方法。

4.2 控制结构分析:在嵌套的for循环中数组访问越界借助于语义分析,数组访问越界(Out of Bounds array access)被自动检测到,而其他的操作都是安全没有错误的。

这个例子表现了语义分析对控制结构分析的能力。

如果不修复,该错误将导致数据冲突,需要大量的调试时间。

4.3 内部过程分析:除数为0借助于语义分析,函数foo中的除数可能为0的错误被自动检测到,而其他的操作都是安全没有错误的。

这个例子表现了语义分析对内部过程分析的能力,能够明确的标示出错误的函数调用和正确的函数调用。

如果不修复,除数为0的错误可能导致处理器宕机,同时由于递归的使用,要调试解决该错误,需要大量的调试时间。

5为什么PolySpace可以取代传统的代码覆盖率的测试?传统的测试理论中,测试结果一般都对代码覆盖率作了要求,要求必须达到90%或者更高。

让我举一个简单的例子来说明代码覆盖率的局限。

static void Recursion (int* depth)97 /* if depth<0, recursion will lead to division by zero */98 { float advance;99100 *depth = *depth + 1;101 advance = 1.0/(float)(*depth - 6); /* potential division by zero */ 102……………..}在做代码覆盖率测试时,只需要一个测试用例*depth = 10;就可以使以上的代码覆盖率达到100%。

如果只是追求高覆盖率尽快达到要求的话,就很可能漏过*depth = 5 会导致除数为0这样的致命的错误。

当然实际的代码可能会更复杂,不会那么简单。

假如GlobalFlag是一个被多个任务使用的全局变量,*depth = 10这一个测试用例可能也会让代码覆盖达到100%,但这个100%能保证下面代码中的除数不为0吗?static void Recursion (int* depth)97 /* if depth<0, recursion will lead to division by zero */98 { float advance;99100 *depth = *depth + 1;101 advance = 1.0/(float)(*depth - GlobalFlag); /* potential division by zero */ 103……………..}我认为覆盖应该分为两种,一是传统意义上说的结构覆盖,即逻辑覆盖(语句覆盖、分支覆盖等)和以及基本路径覆盖,第二是数据输入覆盖。

相关主题