诸如基于模式的静态代码分析、运行时内存监控、单元测试和流分析等自动化技术可以一起用于查找嵌入式 C 应用程序中的错误。以下讨论将使用 Parasoft C/C++test 演示这些技术,Parasoft C/C++test 是一种集成解决方案,用于自动化广泛的最佳实践,以提高 C 和 C++ 软件开发团队的生产力和软件质量。
示例传感器应用
可以在 ARM Cortex-M3 板上运行的简单传感器应用程序的上下文中探索推荐的错误发现策略。应用程序已创建并上传到开发板,但在运行时,它不会在 LCD 屏幕上呈现预期的输出。
它不起作用,原因尚不清楚。在目标板上进行调试既费时又乏味,因为需要手动分析调试器结果以尝试确定真正的问题。或者,可以应用某些工具或技术来自动查明错误。
此时,两个选项是使用调试器调试应用程序或应用自动化测试策略从代码中剥离错误。如果应用自动化技术后应用程序仍然无法工作,调试器可以作为最后的手段。
基于模式的静态代码分析
应用了基于模式的静态分析,而不是调试,它快速、易于使用并且几乎可以应用于每次代码 更改。通过执行静态分析确定了一个问题(参见图 1)。
图 1:静态代码分析识别 MISRA 编码标准违规。
这违反了 MISRA 规则,即在布尔表达式中使用赋值运算符可能会有风险。目的不是使用赋值运算符,而是使用比较运算符。所以这个问题得到解决,程序重新运行。
由于一些输出显示在 LCD 上,因此有所改进。但是,应用程序因访问冲突而崩溃。再一次,有一个选择:使用调试器或继续应用自动错误检测技术。鉴于自动错误检测在发现此类内存损坏方面非常有效,因此执行运行时内存监控是最佳选择。
整个应用程序的运行时内存监控
可以通过应用适合在目标板上运行的轻量级仪器来执行运行时内存监控。上传并运行检测的应用程序并下载结果后,会报告错误(参见图 2)。
图 2:运行时内存监控报告读取超出范围的数组。
这表明在第 48 行读取了一个超出范围的数组。显然,msgIndex变量的值一定超出了数组的范围。向上堆栈跟踪显示,这个具有超出范围值的打印消息是由于在调用函数printMessage()之前为其设置了不正确的条件而导致的。这可以通过放松if语句中的值范围控制并去掉不必要的条件(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() 函数没有被覆盖,并且调用reportSensorFailure的mainLoop函数内部的一个分支根本没有被执行(再次参见图 2)。测试此代码的一种方法是创建一个单元测试(用于mainLoop函数)和一个用户存根(用于readSensor函数),以模拟在功能测试期间难以重现的条件。
带有运行时内存监控的单元测试
创建一个测试用例骨架,然后用测试代码填充。 此外,为readSensor函数添加了一个存根以模拟读取错误。运行测试用例——只执行这个以前未测试的功能——启用运行时内存监控。结果显示该函数现在已被覆盖,但报告了新的错误(参见图 3)。
图 3:启用运行时内存监控的单元测试会暴露内存错误。
测试用例发现了更多与内存相关的错误。调用失败处理程序时,内存初始化(空指针)存在明显问题。进一步分析表明,reportSensorValue()中混合了调用顺序,因此finalize()在printMessage()被调用之前被调用,但finalize()实际上释放了printMessage()使用的内存。
void finalize()
{
if (messages) {
free(messages[0]);
free(messages[1]);
free(messages[2]);
}
free(messages);
}
void printMessage(int msgIndex, int value)
{
const char* msg = messages[msgIndex];
printf(“Value: %d, State: %s\n”, value, msg);
fflush(stdout);
}
void reportSensorFailure()
{
finalize();
printMessage(ERROR_MSG, 0);
}
这个顺序是固定的,测试用例会重新运行一次。
这解决了报告的错误之一。下一步是解决报告的第二个问题:打印消息中的 AccessViolationException。这是因为这些表消息未初始化。为了解决这个问题,在打印消息之前调用initialize()函数。修复后的功能如下:
void reportSensorFailure()
{
initialize();
printMessage(ERROR, 0);
finalize();
}
重新运行测试时,只报告一个任务:一个无效的单元测试用例,这并不是真正的错误。必须验证结果才能将此测试转换为回归测试(参见图 4)。
图 4:必须为回归测试配置测试。
接下来,再次运行整个应用程序。覆盖率分析显示几乎整个应用程序都被覆盖了,结果表明没有出现内存错误问题。
即使运行了整个应用程序并为未覆盖的函数创建了单元测试,但仍有一些路径未被覆盖。可以继续使用单元测试创建,但需要一些时间才能覆盖应用程序中的所有路径。相反,可以使用流量分析来模拟这些路径。
流量分析
运行流分析以模拟通过系统的不同路径,并检查这些路径中是否存在潜在问题。报告了几个问题(参见图 5)。
图 5:流分析发现路径中的几个问题。
有一条潜在的路径——一个未被覆盖的路径——在finalize()函数中可以有一个双重释放。reportSensorValue()函数调用finalize(),然后finalize ()调用free()。此外,在mainLoop()中再次调用finalize () 。这可以通过使finalize()更智能来解决:
void finalize()
{
if (messages) {
free(messages[0]);
free(messages[1]);
free(messages[2]);
free(messages);
messages = 0;
}
}
然后再运行一次流量分析。仅报告了两个问题(参见图 6)。
图 6:流量分析检测到两个剩余问题。
此处可能正在访问索引为-1的表。这是因为积分索引最初设置为 -1,并且在调用printMessage()之前,可能存在一条通过if语句未将此积分设置为正确值的路径。运行时分析并没有导致这条路径,并且这条路径可能永远不会在现实生活中被采用。与实际运行时内存监控相比,这是流分析的主要弱点。流分析显示潜在路径,不一定是在实际应用程序执行期间将采用的路径。通过删除不必要的条件(value 》= 0)可以轻松修复此潜在错误。
void handleSensorValue(int value)
{
initialize();
int index = -1;
if (value 《= 10) {
index = VALUE_LOW;
} else {
index = VALUE_HIGH;
}
printMessage(index, value);
}
报告的最终错误以类似的方式修复。现在,重新运行流分析时,不会报告任何问题。
回归测试
为确保一切正常,重新运行整个分析。首先,应用程序在运行时内存监控下运行,一切似乎都很好。然后使用内存监控运行单元测试并报告一个任务(参见图 7)。
图 7:单元测试发现回归失败。
单元测试检测到reportSensorFailure()函数的行为发生了变化。这是由finalize()中的修改引起的,这是为了纠正先前报告的问题之一而进行的更改。此任务引起对更改的注意,并指示必须审查测试用例。然后要么更正代码,要么更新测试用例,以表明这个新行为实际上是预期的行为。看了下代码,很明显后者为真,并且更新了断言的条件。
void sensor_tests_test_reportSensorFailure()
{
{
messages = 0 ;
}
{
reportSensorFailure();
CPPTEST_ASSERT(0 == ( messages ));
}
}
作为最后的健全性检查,整个应用程序自行运行,在集成开发环境中构建它,无需任何运行时内存监控。结果证实它按预期工作。
补充工具
所有应用的测试方法——基于模式的静态代码分析、内存分析、单元测试、流分析和回归测试——不相互竞争,而是相互补充。一起使用,它们提供了一个非常强大的工具,可以为嵌入式 C 软件提供无与伦比的自动错误检测水平。
作者:Marek Kucharski,Mirosław Zieli nski
审核编辑:郭婷
评论
查看更多