GCC 编译器前端的工作完成后,词法语法分析器已经识别完程序的所有特征,因此将词法、语法分析至Gimple 这个阶段作为代码插桩的切入点是完全可行的。然后,GCC 利用中间代码生成会汇编代码时,如果扫描到RTL 中的特殊节点就会根据用户的需要适当的插入一些完成信息采集功能的汇编代码行,从而就可以实现代码插桩。但是这种做法有两个缺点:一是代码的插桩和编译器的结合很紧密,并且在汇编代码的生成过程中需要针对不同的CPU 生成不同的汇编代码,与CPU 的关联性很强,不便于移植;而是,当程序很大时,探针的植入会造成代码的膨胀,及进行信息采集的代码的插入就需要很多时间。
由于代码插桩技术中插桩点识别过程中的词法、语法分析只需要识别有限的程序结构特征即可,而对程序中所有的词法语法进行分析是因为由中间代码生成汇编代码时,需要以词法语法分析作为基础,识别出所有的程序结构特征。由此可以知道满足插桩技术要求的词法语法分析器可以比中间代码生成的词法语法分析器简单。生成满足插桩点识别的词法语法分析器的词法语法分析程序的输入为预处理后的源代码文件,输出是插桩后的源代码文件(如图1 所示的灰色部分)。由于新增加的词法语法分析程序仅仅是针对插桩所需识别的词法、语法进行分析,故而需要植入的探针比较少,代码膨胀率自然减小,插桩速度加快,进而整个编译过程就会加快。
2.3 插桩程序的设计
探针的设计解决了插桩内容的问题,而插桩程序的设计是用来确定插桩位置和插桩策略的,即回答“在哪插”和“如何插”的问题。
(1)插桩位置:
探针的植入要做到紧凑精干,才能保证在做到收集的信息全面而无冗余,减少代码的膨胀率。因此,在确定插桩位置时,要将程序划分,基本的划分方法是基于“块”结构。
按照块结构的划分,探针的植入位置有以下几种情况:
a. 程序的第一条语句;b. 分支语句的开始;c. 循环语句的开始;d. 下一个入口语句之前的语句;e. 程序的结束语句;f. 分支语句的结束;g. 循环语句的结束;除此之外,根据覆盖测试要求的不同,插桩的位置除了上面所说的几种情况外,也会随着覆盖测试要求的不同有所变化。
(2)插桩策略:
插桩策略是解决“如何插”的问题。传统的插桩策略是在所有需要插桩的位置插入探针,在程序运行过程收集所有可能用到得程序信息,将其写入数据库进行分析和处理。这种方法对于大型的程序来说,将会造成相当大的工作量,效率很低,且会造成很大的代码膨胀率。而我们会根据不同的测试要求,每次插入不同的探针,采用相应的插桩策略,这样就减少了代码的膨胀率,保证了程序执行的效率。下面简单介绍几种探针的插桩策略。
语句覆盖探针(基本块探针):在基本块的入口和出口处,分别植入相应的探针,以确定程序执行时该基本块是否被覆盖。
分支覆盖探针:C/C++语言中,分支由分支点确定。对于每个分支,在其开始处植入一个相应的探针,以确定程序执行时该分支是否被覆盖。
条件覆盖探针:C/C++语言中,if, swich,while, do-while, for 几种语法结构都支持条件判定,在每个条件表达式的布尔表达式处植入探针,进行变量跟踪取值,以确定其被覆盖情况。
根据不同测试要求采用不用的插桩策略,每次在不同的位置植入相应的探针,使得每次只是植入有限的探针,这就更大大减少了代码的膨胀率和插桩的速度。
3 实验
本文采用了一个 1000 行的程序作为被测程序,分别采用使用整体插桩的工具和我们自己开发的工具进行测试,结果发现前者插桩的时间和代码膨胀率分别为3s 和35%,后者插桩的平均时间和平均的代码膨胀率为1s 和8%,插桩时间得到明显提升,代码膨胀率明显减少。
采用以上的程序插桩技术,除了常用的覆盖测试策略外,我们还可以实现MC/DC 和LCSAJ 测试。
4 结束语
本文详细介绍了覆盖测试中的高效代码插桩技术,由此可以看出在实际中覆盖测试分析采用的覆盖策略的多样性决定了程序插桩时需要识别程序的特征的复杂性。同时在软件覆盖测试工具的开发中,如果从软件的分析开始,就有合理的程序划分、适当的选定插桩位置和插桩策略,就可以满足多种测试要求,使得测试能够合理又快速的实现。如果再加上自动化测试工具的支持,那就可以大大提高测试的效率。