代码动态检测实践分享

2018-11-08   出处:大商所行业测试中心  作/译者: 大连飞创  

摘要:代码动态检测是对运行在实际或虚拟处理器上的程序进行的计算机软件分析,可以检测到程序中存在的缓冲区溢出、资源泄露、进程线程异常等问题。代码覆盖是度量软件测试的一种方式,描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率。代码覆盖率的度量方式包括行覆盖率、函数覆盖和分支覆盖率。本文实践在代码动态检测中引入代码覆盖测试,目的是通过分析覆盖率补充测试场景和数据,以保证代码动态检测的充分性。

一、概述

随着计算机应用需求的日益增加,应用程序的设计与开发也日趋复杂。在程序实现的过程中,处理数以千计变量的内存分配和释放,以及大量对计算机内存和存储的并发读写指令在所难免,一旦疏忽就难免引入错误,导致程序运行出错。在实际中经常会遇到服务器长时间运行出现性能下降甚至系统崩溃问题,以及当访问量增大时出现业务处理错误问题等。只依靠黑盒测试和代码静态检测手段很难发现这些问题,即使发现问题还需要花费很大成本进行定位和修复。通过引入代码动态检测,在黑盒测试同时从代码和指令层级对程序进行监控,发现并锁定引发内存错误和线程错误的代码进行修复,极大减少测试成本。再结合代码覆盖测试,从代码行覆盖率、函数覆盖率和分支覆盖率三个角度度量和提高代码动态检测的充分性,增强管理者对软件产品质量的置信水平。

(一)代码动态检测

代码动态检测通过工具监控运行时程序发现潜在的内存错误和进程线程错误。为了有效地发现和定位程序错误,监控工具通常会在被测程序中插入代码监控程序的运行。这种在被测程序中插入监控代码的技术被称为代码插桩。按照代码插桩或替换的时间不同,主要分为三类插桩技术:

ECI(Executable Code Interception):执行码替换技术,在链接或执行时,直接替换相应的系统调用,代表工具如valgrind。

OCI(Object Code Insertion):目标码插桩技术,直接对目标码进行分析,并插入相应的汇编代码,代表工具如Purify。

SCI(Source Code Instrumentation):源码插桩技术,对源程序进行扫描分析,收集所有必要的检测信息,插入相应的检测代码,代表工具如Insure++。

这些插桩技术通常以监控运行时程序使用的资源如内存、文件句柄、组数、线程共享资源等为手段,发现由于资源使用不当导致的内存泄露、文件句柄未关闭、数组访问越界、访问共享资源冲突等问题。这些插桩技术一般不会影响程序的功能,但会使程序的运行速度变慢、占用的内存空间变大。

(二)代码覆盖测试

在软件测试的世界里,代码覆盖测试是衡量测试完整性的重要指标。对于被测系统是否经过了充分的测试,这是一个相当有效的手段,一方面衡量测试工作本身的有效性,另一方面增强管理者对软件产品质量的置信水平。代码覆盖测试通常从代码行覆盖率、函数覆盖率和分支覆盖率三个方面衡量测试的充分性。在本文代码动态检测实践中,通过分析三个覆盖率数据来补充测试场景和数据以提高测试的充分性。在其他测试领域如功能测试、可靠性测试中也可以加入代码覆盖测试以评估和提高测试的充分性。

二、实践分享


在测试大型金融类项目时,考虑金融软件对运行有很高的安全性、实时性和稳定性要求。然而,错误地使用内存资源和线程资源极易引发系统的安全问题和稳定性问题。通过代码动态检测对内存资源和线程资源的使用进行监控,发现并锁定问题,再结合代码覆盖测试保证测试的充分性。

为了做好代码动态检测,在测试前期对测试工具进行了深入调研,从工具发现问题的能力、与被测系统结合的难易程度以及工具的使用成本等多方面综合考虑,最终选择使用Valgrind以及Gcov/Lcov工具开展代码动态检测工作。Valgrind是运行在Linux上一套开源的基于仿真技术的程序调试和分析工具套件,用于C++项目的代码动态检测。通过Valgrind可以发现内存泄露、内存使用越界、使用未初始化内存、重复释放内存、线程死锁、线程数据访问冲突等问题。Gcov/Lcov是Linux上用于测试C++代码覆盖率的开源工具,支持获取代码行覆盖率、函数覆盖率和分支覆盖率。

(一)具体方案


图一:代码动态检测流程图

代码动态检测的实施主要分三个阶段:

第一阶段:测试准备,包括图中的第1、2步。

第1步,编译被测系统,在编译时增加编译参数在被测系统中加入代码覆盖率收集功能;

第2步,部署被测系统,在部署环境时需要正确配置GCOV_PREFIX和GCOV_PREFIX_STRIP环境变量以保证代码覆盖测试正常进行。

第二阶段:测试执行,包括图中的第3、4、5步。

第3步,配置工具参数,依据检测内容选取Valgrind/Lcov工具参数;

第4步,执行动态检测,为提高检测效率需要在功能测试或自动化测试配合下完成代码动态检测;

第5步,补充测试场景和数据,分析代码覆盖测试结果(行覆盖率、函数覆盖率、分支覆盖率)补充测试场景和数据,提高代码动态检测的充分性。

第三阶段:结果分析,包括图中的第6步。

第6步,分析代码动态检测结果,包括代码动态检测结果和代码覆盖测试结果。

(二)难点&解决

难点问题:以后台服务方式持续运行的被测系统,无法生成覆盖率数据文件。

解决办法:这类被测系统的退出方式通常为kill进程,可在kill进程前通过gdb主动调用__gcov_flush函数解决该问题,如下:

gdb --quiet --pid=$pid > /dev/null << EOF

p __gcov_flush()

EOF

Kill -9 $pid

难点问题:如果被测系统部署在多台不同的机器上,为了收集完整的覆盖率需要对各机器的覆盖率进行合并。

解决办法:lcov工具本身支持覆盖率生成和覆盖率合并功能,如下:

覆盖率生成命令及参数:

lcov -c -d gcda_dir -rc lcov_branch_coverage=1 --no-external -o cov.info

覆盖率合并命令及参数:

lcov -a cov1.info -a cov2.info … -rc lcov_branch_coverage=1 -o cov_all.info

(三)发现问题举例

1、多线程数据争用问题,指两个或更多线程同时读/写共享数据。一旦发生数据争用,共享数据的值是不可知的,使用这些值会导致程序运行结果完全不可预测,甚至直接崩溃。

问题代码举例:

4 void *write_data(void *arg)

5 {

6     char *buffer = (char*)arg;

7

8     sprintf(buffer, “I’am thread %ld”, pthread_self());

9     pthread_exit(0);

10    return NULL;

11 }

12

13 int main(void)

14 {

15     char data_buffer[20] = { 0 };

16     pthread a, b;

17    

18     pthread_create(&a, NULL, write_data, data_buffer);

19     pthread_create(&b, NULL, write_data, data_buffer);

20     pthread_join(a, NULL);

21     pthread_join(a, NULL);

22

23     return 0;

24 }

Valgrind检测结果:

==2431== Thread #3 was created

……

==2431==    by 0x4011F1: main (thread.c:19)

……

==2431== Thread #2 was created

……

==2431==    by 0x4011C2: main (thread.c:18)

……

==2431== Possible data race during read of size 8 at 0x6042E0 by thread #3

==2431== Locks held: none

==2431==    at 0x401112: write_data(void*) (thread.c:5)

……

==2431== This conflicts with a previous write of size 8 by thread #2

==2431== Locks held: none

==2431==    at 0x40111D: write_data(void*) (thread.c:5)

……

2、内存泄露问题,指程序动态申请内存资源使用后未归还。一旦发生内存泄露,在程序长时间运行中会不断堆积最终导致内存耗尽,程序崩溃。

问题代码举例:

40 void memory_leak(void)

41 {

42     char *pc = (char*)malloc(1);

43     *pc = ‘T’;

44     

45     char c = *pc;

46     

47     printf(“c = [%c]\n”, c);

48 }

Valgrind检测结果:

3、使用未初始化内存问题,指局部变量或动态申请的变量,其初始值是随机的。一旦使用这些随机值,会使程序的行为变得不可预期,甚至由于访问非法内存而导致程序崩溃。

程序源码:

6   void use_uninit_memory(void)

7   {

8       char *pc;

9       char c = *pc;

10      printf(“c = [%c]\n”, c);

11  }

检测结果:

==3908== Use of uninitialized values of size 8

==3908==     at 0x400FE4: use_uninit_memory() (memory.c:9)

==3908==     by 0x4014A8: main (memory.c:121)

4、使用内存越界问题,指程序使用已申请或已分配范围外的内存。一旦程序中存在使用内存越界问题,可能被恶意代码攻击获取整个程序的控制权,造成严重的安全性问题。

问题代码举例:

28 void use_out_memory(void)

29 {

30     char *pc = (char*)malloc(1);

31     *pc = ‘T’;

32

33     char c = *(pc + 1);

34     printf(“c = [%c]\n”, c);

35

36     free(pc);

37 }

Valgrind检测结果:

==3914== Invalid read of size 1

==3914==    at 0x4010E7: use_out_memory() (memory.c:33)

==3914==    by 0x4014F3: main (memory.c:127)

==3914== Address 0x5ab6c81 is 0 bytes after a block of size 1 alloc’d

==3914==    at 0x4C2DB8F: malloc (in /usr/lib/Valgrind/

vgpreload_memcheck-amd64-linux.so)

==3914==    by 0x4010D7: use_out_memory() (memory.c:30)

==3914==    by 0x4014F3: main (memory.c:127)

(四)覆盖率应用

在代码动态检测中加入代码覆盖测试主要有两个目的:

  • 通过已发现缺陷数和已覆盖代码行数计算被测试程序缺陷密度,用于评估被测系统质量。

  • 通过分析函数覆盖补充测试场景,分析分支覆盖补充测试数据,用于提高代码动态检测的充分性。

下图是代码覆盖测试报告:其中代码行覆盖率为97.9%,函数覆盖率为100%,分支覆盖率为66.7%。

三、结束语


虽然代码动态检测主要依赖检测工具完成测试,但在实施过程中,仍然要求测试人员具备一定的C++代码分析能力和Linux系统操作基础。本文只介绍了代码动态检测整体实施过程,关于实施的具体细节及工具参数的选取未详细阐述,需要读者结合具体项目特点并参考网上相关资料完成。



欢迎给测试窝投稿或参与内容翻译工作,请邮件至editors@testwo.com。也欢迎大家通过新浪微博(@测试窝)或微信公众号(测试窝)关注我们,并与我们的编辑和其他窝友交流。
127°|1272 人阅读|0 条评论

登录 后发表评论
最新文章