查找嵌入式C语言程序/软件中的缺陷的多种技术

发布者:技术掌门最新更新时间:2010-11-25 关键字:缺陷  自动化检测  C++test  函数调用 手机看文章 扫描二维码
随时随地手机看文章

    基于模式的静态代码分析、运行时内存监测、单元测试以及数据流分析等软件验证技术是查找嵌入式C语言程序/软件缺陷行之有效的方法。上述技术中的每一种都能查找出某一类特定的错误。即便如此,如果用户仅采用上述技术中的一种或者几种来进行验证,这样的验证方法很有可能会漏过对程序中的一些缺陷的检查。解决此类问题的一种安全和有效的策略就是同时使用上述软件验证中的所有互补技术。这样就能建立起一个牢固的框架来帮助用户检查出可能会避开某种特定技术的缺陷。与此同时,用户也自然地建立起一个能检测出关键并且难以查找的功能性错误的环境。

    本文将详尽阐述基于模式的静态代码分析、运行时内存错误检测、单元测试以及数据流分析等自动化技术共同使用时是如何查找出嵌入式C语言程序/软件中的缺陷的。本文中将以Parasoft C++test为例来演示上述各项技术。C++teST是一个经广泛的最佳实践证明能提升软件开发团队开发效率以及软件质量的自动化集成解决方案。

    当读者在阅读本文以及任何时候思考查找到的缺陷时,关注文中的截图是很重要的。自动化检测例如内存崩溃和死锁的缺陷,毫无疑问对任何开发团队都是一项必不可少的任务。尽管如此,最致命的缺陷却是功能性错误,这往往是难以自动发现的。在本文的结论部分我们将简要地讨论一下查找这些缺陷的技术。

情景简介
 
    为了给出一个具体的示例,我们将就一个我们最近遇到的案例来介绍以及演示我们所推荐的缺陷查找策略:一个运行在ARM 板上的简单传感器应用程序。

    假设我们已经创建了该应用系统,但是当我们将程序上载到系统目标板上并试图运行该程序时,我们没有在LCD屏上看到所预期的输出。

    我们尚不明确系统不能正常工作的原因,因此我们设法对系统进行调试,但是在目标板上进行调试是一件耗时而且烦人的事。因为我们不得不手动分析调试器的结果并试图人工判断出问题的真正原因。或者我们使用一些被证实能自动定位出错误的工具或技术来帮助我们减轻负担。

    从这一点而言,我们要么期待使用调试器来调试程序能够带来好运,要么我们尝试使用一种自动化的测试策略来查找代码中所存在的错误。如果自动化技术仍然没有帮助我们查找到错误,那么我们不得不回到使用调试器作为最后的办法。

基于模式的静态代码分析

    这里,我们假设仅在绝对必要的情况下才使用调试器进行调试,因此我们从运行基于模式的静态代码分析开始。它将查找到如下图所示的问题:

    这是违反了 MISRA 的一个规则,此违规说明该处的赋值运算符存在一些可疑情况。的确,编程者此处的本意是使用比较运算符而不是赋值运算符。因此我们将此处检测到的冲突修改掉,并重新运行程序。

   我们发现有了一些改善:一些输出被显示在了LCD屏上了。但是,由于一次访问违规,程序崩溃掉了。因此我们需要再次地做出选择。我们是应该使用调试器还是继续使用自动化的错误检测技术。由于经验告诉我们自动化错误检测技术能非常高效地检查出我们当前程序所遇到的内存崩溃这类问题,因此我们决定使用运行时内存监测来查找问题。

整个程序的运行时内存监测

    为了进行运行时内存监测,我们使用 C++test 来插装应用程序。这样的插装是轻量级的,所以经过插装后的程序适合在目标板上运行。当我们把程序上载到目标板上并运行经过插装的程序后,我们将结果下载到PC上,如下的错误将被报告出来:

    该结果指出在第48行代码处产生了一次读取数组越界的错误。显然,msgIndex变量的值肯定超过了数组的范围。如果我们随着堆栈追踪上一级的原因,我们将发现此处的打印信息所指示的值的确超出了数组的范围(因为在调用printMessage()函数前我们给出了一个错误的条件)。我们可以删除掉这个不必要的条件(value <= 20)以修改这个错误。

void handleSensorValue(int value)
{
initialize();
int index = -1;
if (value >= 0 && value <= 10) {
index = VALUE_LOW;
} else if ((value > 10) && (value <= 20)) {
index = VALUE_HIGH;
}
printMessage(index, value);
}

    然后我们重新运行程序,将不会再报告任何内存错误。当我们把程序上载到目标板上时,它似乎如我们预期那么在工作了。尽管如此,我们仍然有一些担心。

    我们仅查找到我们所执行的代码路径中的一个内存写溢出实例,我们凭什么能够断定我们尚未执行到的代码就不会有内存写溢出错误了呢?如果我们检查覆盖率分析,我们就会发现reportSensorFailure()这个函数从未被执行到。我们有必要对这个函数进行测试,但是具体如何进行呢?建立一个调用该函数的单元测试用例就是一个不错的办法。

    在单元测试中使用运行时内存监测:我们使用C++test的测试用例向导来创建一个测试用例的框架,并向其中添加一些测试代码。然后运行该测试用例——以检查上面提到的未经测试的函数,同时打开运行时内存监测功能。使用C++teST,全过程大约只需要数秒钟。

    结果标明该函数已经被覆盖到了,但同时也查找到了新的错误:

    我们的测试用例查找到了更多的内存相关错误。很显然,当失败处理函数被调用时,我们的内存初始化存在问题(空指针)。通过更进一步的分析,我们发现在reportSensorValue()函数中存在函数调用顺序错误。

    finalize()函数先于printMessage()函数被调用,但是finalize()函数中释放了printMessage()函数需要使用的内存。

void finalize()
{
if (messages) {
free(messages[0]);
free(messages[1]);
free(messages[2]);
}
free(messages);
}

    将函数调用顺序进行修改后,我们重新运行程序。

    这样我们就解决了上面报告中的第一个错误。现在我们再来分析报告中的第二个错误:即打印信息中的AccessViolatiONException。产生这个错误的原因是相应的消息列表未经初始化。为了解决该问题,我们在打印该信息前调用一次initialize()函数来对其进行初始化。经修改后的函数如下所示:

void reportSensorFailure()
{
initialize();
printMessage(ERROR, 0);
finalize();
}

    当我们再次运行该测试用例时,仅有一个任务被报告出来:未经验证的单元测试用例(an unvalidated unit test case),这其实并不算一条错误。我们只需对输出进行一下验证,以将该测试用例转换为回归测试。通过创建合适的断言,C++test会自动为我们完成这些步骤。

    接下来我们再次运行整个程序。覆盖率分析告诉我们几乎整个程序都已经被覆盖到了,并且没有发现任何内存错误。

    这样就结束了吗?其实不然。虽然我们运行了整个程序并为未覆盖到的函数创建了单元测试用例,但还是有一些路径是没有被覆盖到的。我们仍然可以继续创建单元测试用例,但是若指望通过这样的方法来覆盖程序中的所有路径将耗费相当长的时间。或者我们使用另外的方法,使用数据流分析来对这些路径进行模拟。

数据流分析

    我们使用C++test的BugDetective来进行数据流分析,BugDetective能模拟系统中的不同路径并检查这些路径中是否存在潜在的问题。进行数据流分析后,我们得到如下结果:

    仔细分析报告的结果,我们发现程序中存在一条未被覆盖到的潜在路径可能会造成在finalize()函数中出现两次free的操作。在程序中,reportSensorValue()函数调用了finalize()函数,然后finalize()函数调用了free()。同时,finalize()函数还会被mainLoop()函数调用。我们可以修改finalize()函数以使其更加智能化,从而修复这个问题,修改后的代码如下:

void finalize()
{
if (messages) {
free(messages[0]);
free(messages[1]);
free(messages[2]);
free(messages);
messages = 0;
}
}

[page]

    现在我们再次运行数据流分析,得到的结果将只有两个问题:

    这里我们可能使用了-1作为索引来访问了数组。这是由于整型变量index被设置的初始值为-1,并且存在一条可能通过if语句的路径在未将该整型变量正确的进行初始化之前便调用了printMessage()函数。运行时分析未检查到这样的一条路径,并且该路径很有可能在真实世界中永远不可能被执行到。这就是静态数据流分析相对于运真实运行时内存监测最主要的不足:数据流分析能检查出潜在的路径,这些路径可能包含在程序实际执行过程中不会执行到或不存在的路径。尽管如此,为了做到有备无患,我们删除了上述的不必要的条件(value>=0)以修改这个潜在的错误。

void handleSensorValue(int value)
{
initialize();
int index = -1;
if (value <= 10) {
index = VALUE_LOW;
} else {
index = VALUE_HIGH;
}
printMessage(index, value);
}

    相同地,我们也对最后一个报告的错误进行相应的处理。现在我们再次运行数据流分析,将不会再有错误被报告出来。

    为了确保程序运行一切正常,我们重新运行整个分析过程。首先,我们开启运行时内存监测并运行应用程序,一切表现正常。然后我们开启内存监测并运行单元测试,一个任务被报告出来:

    我们的单元测试检测到reportSensorFailure()函数的行为已经发生了改变。这是由于我们已经对finalize()函数进行了修改——为了纠正之前报告的一个问题所做的修改。此处报告的任务是为了让我们注意此修改,并提示我们应该对测试用例进行相应的审查,并且确定是否应该对代码或者测试用例进行相应的修改,以表示这种新的行为实际上是我们所预期的行为。在检查完代码之后,我们发现后者(修改)是正确的并且应该更新断言的正确条件。

/* CPPtest_TEST_CASE_BEGIN test_reportSensorFailure */
/* CPPTEST_TEST_CASE_CONTEXT void reportSensorFailure(void) */
void sensor_tests_test_reportSensorFailure()
{
/* Pre-condition initialization */
/* Initializing global variable messages */
{
messages = 0 ;
}
{
/* Tested function call */
reportSensorFailure();
/* Post-condition check */
CPPTEST_ASSERT(0 == ( messages ));
}
}
/* CPPTEST_TEST_CASE_END test_reportSensorFailure */

    作为最终的确认,我们需要独立地运行整个程序——在IDE中关闭掉运行时内存监测来对程序进行构建。结果显示一切如我们所预期一样运行。

总结

    作为全文的结尾,让我们一起对上述各个步骤进行一个鸟瞰式的总结。

    首先,我们开发的程序并未如我么所预期那样运行,我们不得不在两种解决方法中选择一种来查找程序中的错误:通过运行调试器或者使用自动错误检测技术。

   如果我们使用调试器运行代码来查找错误,我们将会看到一些很奇怪的现象:程序中的一些变量总是被赋予了相同的值。基于这种现象我们不得不通过排除法来查找问题的原因——即在应该使用比较运算符的地方我们错误地使用了赋值运算符。而静态代码分析则能为我们自动地检查出该逻辑错误。运行时内存分析是不可能检查出这种错误的,因为这种错误与内存无关。数据流分析也很有可能找不到这类错误因为数据流分析仅仅是通过这些路径而不会验证这些条件的正确性。

    当我们解决了这个问题后,程序可以运行了,但是仍然还有内存相关的问题。内存相关的问题是很难被调试器发现的;当用户使用调试器调试程序时,用户并不知道内存的实际大小。但是自动错误检查工具能够做到这点。因此,为了查找这些内存问题,我们将整个程序进行插装,并使用运行时内存分析工具来运行程序。这样我们就能知道到底是那一片内存发生了写溢出错误。

    尽管如此,在审查覆盖率分析结果的时候,我们注意到在目标板上测试的时候,并不是全部代码都被覆盖到了。通过自动化的工具得到这样的覆盖率信息是简单的,因为工具会自动地跟踪覆盖率,但是,如果我们是通过调试器,就不得不判断哪一部分程序经过了验证。而这通常只能依靠我们人工记录的方式来实现。

    当工具提醒我们一些代码未被覆盖到时,我们决定改变单元测试来额外地增加我们测试执行的覆盖率。这就揭示了程序中另外一些问题。在目标系统的正常测试中,覆盖所有函数也许是不可能完成的任务,因为其中一些函数可能是硬件的失败处理函数或仅在某些小概率的特定情况下才会被调用的函数。而对这些函数的测试对于一些注重安全性的程序而言又是至关重要的。试想在飞机上用来处理速度传感器问题的程序中存在着代码错误:我们会有系统崩溃的危险,而不是导致某个设备为非工作状态。因此,通过创建单元测试用例来覆盖这类型的执行路径往往是对其进行有效测试的唯一方法。

    接下来,我们修复了工具检查到的所有问题,同时通过验证相应的结果创建了一个回归测试用例(作为报告的任务之一引导我们完成)。然后我们运行数据流分析来覆盖在目标系统上即便使用单元测试也未执行到的路径。在此之前,我们几乎已经达到了100%的代码行覆盖率,但是我们的路径覆盖率却未达到这个水平。BugDetective帮我们发现了这些方面的一些潜在问题。这些问题可能并没有实际发生或者有可能永远不会发生。也许在实际运行时,这些问题仅仅会在当其条件满足的情况下才会出现,并且在现实生活中,这些条件可能永远不可能满足。尽管如此,我们不能保证随着代码的升级,应用程序不会执行到这些路径。

    安全起见,我们仍然修改了所报告的问题以排除任何可能影响它的实际应用执行的风险。在修改代码的同时,我们同时也引入了回归测试,当我们再次运行单元测试时立即被检测到。在所有的自动化错误检测方法中,回归测试是唯一能够帮助我们检查到代码是否发生了功能性的改变的方法,并且能验证出对代码进行的修改是否引入了功能性的错误以及不可预知的副作用。最后,我们修改了回归测试套件,并重新测试代码,发现一切运行正常。

    正如读者所见,我们使用的一切测试方法——基于模式的静态代码分析、内存分析、单元测试、数据流分析以及回归测试——并不是相互竞争的关系,恰好相反,它们是一种互补的关系。将上述工具结合使用,它们就是一套具有强大作用的工具集,并为嵌入式C语言程序/软件提供一个无可比拟的自动化错误检测解决方案。

    总而言之,通过自动地查找很多关于内存和其它编码的缺陷,我们成功地让程序运行起来了。尽管如此,值得注意的是,最危险的缺陷却是实际的功能性错误:例如程序并未如所指定的要求运行。而不幸的是,这些错误往往是非常难以被发现的。

    查找这类缺陷的最好的一个方式就是通过同行代码审查来实现。即另指派至少一人来检查代码并且审查代码与需求内容的一致性,这样用户就能对实际程序是否会如预期那样运行有一个很好的*估。

    另外一个十分有用的策略是围绕代码创建一个回归测试套件,这能帮助用户快捷地验证代码与规范的一致性。在本文所描述的示例情景中,单元测试被用来强制执行应用程序级的运行时内存监测所未覆盖到的代码:它能覆盖到当前程序的功能性,在此之后,我们对代码做了一些修改,它能提醒我们代码出现的相应的功能性问题。事实上,这种单元测试用例应该被更早地创建起来:理想情况下,当用户在实现程序的功能时就应该被创建起来。这样,用户就能得到更高的覆盖率并同时构建起一个更强壮的“安全网”来捕捉关键的功能性改变。

    Parasoft的C++test能帮助用户完成这两个任务:从自动化到管理同行代码审查流程,以及帮助团队创建,持续地运行并维护一个高效的回归测试套件。

关于Parasoft C++test

    Parasoft C++test是一个经广泛的最佳实践证明能提升软件开发团队开发效率以及软件质量的自动化集成解决方案。C++test能进行诸如编码策略增强、静态代码分析、运行时内存监测、自动同行代码审查以及单元和组件测试,从而为软件开发团队提供一种更加实用的方法来确保其C以及C++程序能如所预期那样工作。C++test可以用于在通用开发IDE下的桌面平台中,以及在回归测试时通过命令行以批处理模式的方式运行。同时,C++test还集成了Parasoft的报告系统,该系统能提供具有细分能力的基于Web 的仪表板,这使得开发团队根据C++test的测试结果和其他的一些关键进程指标来更加方便地跟踪项目的状态和趋势。

    通过在宿主机上进行大量的测试以及在目标系统中进行的平滑的验证,C++test能够帮助软件开发团队减少花在嵌入式系统开发中的时间、精力以及成本。随着代码在宿主机上的构建,C++test的自动化框架使得开发者能在目标硬件系统尚未准备好的情况下就开始测试以提升代码质量。这大大地缩短了花在目标系统上测试的时间。早期在宿主机上构建的测试套件可以被重用来在仿真器或真实的目标板上验证程序的功能性。

关键字:缺陷  自动化检测  C++test  函数调用 引用地址:查找嵌入式C语言程序/软件中的缺陷的多种技术

上一篇:基于英特尔凌动E600的板卡秀
下一篇:意法半导体与IAR Systems共同宣布软件开发工具支持SPEAr®微处理器

推荐阅读最新更新时间:2024-05-02 21:12

谷歌同意赔偿麦克风缺陷的Pixel用户
据外媒报道,谷歌已同意就2016款谷歌Pixel的所有者提起的集体诉讼达成和解。这些所有者称,谷歌故意销售麦克风存在缺陷的手机。 2017年3月,谷歌第一次承认部分手机存在问题,谷歌当时表示,只有不到1%的Pixel手机“音频编解码器的焊接处出现了细微的裂纹”。手机的通话和语音助手功能因此出现问题后,谷歌表示将采取“额外措施来加强焊接连接”。但不到一年后,谷歌就因为仍在销售存在问题的设备被一些愤怒的用户告上法庭。 为了确定每个人的具体赔偿金额,和解方案将2016款Google Pixel和Google Pixel XL的所有者分为四类。将麦克风有缺陷的Pixel返还给谷歌但新设备依然存在同样缺陷的用户,将获得最高金额(5
[手机便携]
arm汇编语言调用C函数之参数传递
对于ARM体系来说,不同语言撰写的函数之间相互调用(mix calls)遵循的是 ATPCS(ARM-Thumb Procedure Call Standard),ATPCS主要是定义了函数呼叫时参数的传递规则以及如何从函数返回,关于ATPCS的详细内容可以查看ADS1.2 Online Books Developer Guide的2.1节。这篇文档要讲的是 汇编代码中对C函数调用时如何进行参数的传递以及如何从C函数正确返回。 不同于x86的参数传递规则,ATPCS建议函数的形参不超过4个,如果形参个数少于或等于4,则形参由R0,R1,R2,R3四个寄存器进行传递;若形参个数大于4,大于4的部分必须通过堆栈进行传递。 我们
[单片机]
C51学习心得体会,函数的传引用调用和传值调用方法
传值调用建立参数的一份拷贝并把它传给调用的函数,在调用函数中修改参数值的拷贝不影响原始的变量值;传引用调用允许调用函数修改原始变量的值。 C语言用指针*和间接引用运算符&模拟传引用调用,数组会自动模拟传引用调用。传引用调用可以在被调用函数中修改调用函数环境中的参数变量,传值调用保护数据。 e.g. (1)传值调用 int cubeByValue(int); main() { int num=5,result; result=cubeByValue(num); } int cubeByValue(int n) { return n*n*n; } (2)传引用调用 int cubeByValue(int *); main() {
[单片机]
双晶探头的正确使用方法及射频方式检测表面缺陷
近日,不少客户在利用双晶探头对薄板母材探伤的过程中,打电话给我们咨询如何正确选择和使用双晶探头探伤。本文分三个部分和大家一起探讨“双晶探头的结构和工作原理”、“如何正确选择双晶探头”、“射频检波方式的表面探伤应用”。 1、双晶探头的结构和工作原理 一般来讲,一个探头壳体内装有两个晶片的探头我们称之为双晶探头,又称分割式探头。由两个纵波晶片组合成的双晶探头称为纵波双晶探头,又称双晶直探头;由两个横波晶片组成的双晶探头称为横波双晶探头,又称双晶斜探头。 这两种双晶探头中,双晶直探头的应用较为广泛,以下以双晶直探头为例重点探讨双晶直探头的结构和工作原理: 双晶直探头的两个纵波晶片一个用于发射超声,一个用于接收超声(图一)。发射
[测试测量]
爱普科斯温度传感器实现零缺陷,首获“质量金奖”
TDK公司宣布,爱普科斯(EPCOS) 再次荣获由欧洲领先的家用电器制造商博世和西门子家用电器集团(BSH) 颁发的“质量金奖”。此奖项代表了BSH对爱普科斯温度传感器卓越品质的肯定。目前,爱普科斯传感器已应用于BSH公司制造的电磁炉上。 BSH质量金奖的评定基于其对全欧洲约800个供应商制订的高质量目标进行。爱普科斯 NTC温度传感器由印尼的Batam工厂生产,产品达到了百万分之零(0 ppm)的缺陷率,远低于当前BSH制定的50 ppm的标准。 BSH在全球拥有约46,000多名员工,销售额超过98亿欧元,在家电市场的排名为欧洲第一,是全球领军者之一。 关于TDK公司 TDK公司是一家领先的电子公司,总部
[传感器]
国产SiC衬底致命缺陷降80%
近日,国内一家SiC衬底厂商取得了车规级的技术成果。 据山西天成半导体介绍,他们新开发的6英寸导电型SiC衬底的缺陷得到了非常好的抑制,其中部分TSD缺陷密度低至0个/cm2,而部分致命性缺陷BPD则低至32个/cm2。而在量产过程中,可以做到65%的衬底产品TSD<100个/cm2、BPD<100个/cm2。 相较于通常的车规级要求(500个/cm2),山西天成半导体的SiC衬底BPD缺陷降低了80%,而相比大多数企业(约800个/cm2),他们的BPD缺陷降低了87.5%左右。 该司表示,他们这一技术成果将有助于提升国产SiC MOSFET的“上车”进程和竞争力,增强汽车企业对国产SiC半导体的信心。
[汽车电子]
国产SiC衬底致命<font color='red'>缺陷</font>降80%
基于机器视觉的太阳能电池片外观缺陷检测
引言 随着国内外对清洁能源需求的增加以及各国政府对清洁能源补助的提高,光伏组件的需求也在快速增长。为保证产能及组件品质的可靠性,高精度、高速太阳能电池片的全自动焊机成为光伏企业的首选。目前这些设备大多依赖进口,然而进口设备高昂的价格很大程度上增加了太阳能发电的成本,急需研制出高水平的太阳能电池片焊接设备来满足市场的需求。电池片焊接设备的精度、速度与电池片的完整性相关。传统的检测方法精度低、速度慢,而且部分还需依赖人工操作,不能满足市场要求,而基于机器视觉的检测方法能有效地解决这些问题。 机器视觉技术与人类通过眼睛获取信息的方式是一致的,光学图像的采集就好比是机器在用“眼睛”获取信息,检测算法就是机器在用“大脑”思考的过程。
[单片机]
基于机器视觉的太阳能电池片外观<font color='red'>缺陷</font><font color='red'>检测</font>
电源设计小贴士:MLC电容器常见缺陷的规避方法
因其小尺寸、低等效串联电阻(ESR)、低成本、高可靠性和高纹波电流能力,多层陶瓷(MLC)电容器在电源电子产品中变得极为普遍。一般而言,它们用在电解质电容器leiu中,以增强系统性能。相比使用电解电容器铝氧化绝缘材料时相对介电常数为10的电解质,MLC电容器拥有高相对介电常数材料(2000-3000)的优势。这一差异很重要,因为电容直接与介电常数相关。在电解质的正端,设置板间隔的氧化铝厚度小于陶瓷材料,从而带来更高的电容密度。 温度和DC偏压变化时,陶瓷电容器介电常数不稳定,因此我们需要在设计过程中理解它的这种特性。高介电常数陶瓷电容器被划分为2类。图1显示了如何以3位数描述方法来对其分类,诸如:Z5U、X5R和X7R等。例如,
[模拟电子]
电源设计小贴士:MLC电容器常见<font color='red'>缺陷</font>的规避方法
小广播
最新嵌入式文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved