测试工程师经常面对的一个问题就是如何获得测试的代码覆盖率。很多专业软件可以提供这种专门的代码覆盖率检测。通过对 GDB 的小小改造,也可以令其提供代码覆盖率测试功能。这种改动与平台无关,只要 GDB 支持的平台,都可以运行。
简介
熟悉 Excel 的程序员都知道,Excel 不仅是一个应用软件,还能作为一个开发平台。这不仅是因为 Excel 提供了 VBA,更重要的是 Excel 本身处理了数据库连接,数据处理以及报表生成等复杂的工作。程序员从而避免了自己实现这些功能的负担。
同样,我们认为 gdb 本身的强大功能也使得它可以成为一个开发平台,充分利用它的符号处理能力和进程控制功能,我们可以开发出一些新的功能。
测试工程师经常面对的一个问题就是如何获得测试的代码覆盖率。很多专业软件可以提供这种专门的代码覆盖率检测。通过对 GDB 的小小改造,也可以令其提供代码覆盖率测试功能。这种改动与平台无关,只要 GDB 支持的平台,都可以运行。
基本原理
GDB的一个基本功能就是单步运行程序,我们想到,如果在每次单步运行的时候,记录下运行过的代码数量,将此数据与总代码段长度比较,不就可以获得代码覆盖率了吗?
最初的想法很简单,但是让测试人员不停地单步执行显然是不现实的,因此我们扩充了基本的gdb命令,增加了一条命令叫做covertest。该命令不断地自动调用单步执行命令,并在每一个单步命令之后,记录下运行过的代码行数。直到程序运行结束。然后covertest命令读取ELF文件头,得到总的代码段长度。最后,用记录下的运行过的代码数量除以总的代码段长度,从而得到代码覆盖率。
经过几周的调试,我们在RedHat9.0/x86平台上,修改GDB5.3,成功地实现了代码覆盖率测试功能。
代码覆盖率定义和代码长度
我们把代码覆盖率定义为运行过的代码长度除以程序总的代码长度。
代码长度是二进制代码长度。而不是在C源文件中的代码长度。比如一条赋值语句在C语言中就是一条语句,但是编译为汇编语言后可能是一条,也可能是多条汇编指令。而且在Intel IA处理器中,指令长度是可变的。因此我们所说的代码长度是指最终的二进制代码的字节长度。
这种定义可能不是最佳的定义.但是是最容易实现的定义。在本文中,代码覆盖率采用机器指令长度作为衡量标准。
下面的例子比较了不同的代码长度的定义:
增加命令covertest
gdb是一个命令行工具,它基本的工作模式类似Shell。接收用户输入的命令然后执行相应的处理函数。gdb中CLI(command line interface)子系统负责用户界面的工作,它显示提示符,接收用户输入,分析用户输入并调用相应的处理函数。
CLI子系统的设计非常完善,它为用户添加新命令提供了几个专门函数。add_com()就是最基本的一个。它有四个入口参数,第一个参数是命令的名字,类型为字符串;第二个参数表明该命令的类型;第三个参数是该命令的处理函数,第四个参数是关于该命令的帮助说明。:
struct cmd_list_element *add_com (char *name, enum command_class class, void (*fun) (char *, int), char *doc) |
下面的代码显示了如何添加新的gdb命令.
_initialize_mark (void) { struct cmd_list_element *c; c = add_com("covertest",class_breakpoint,set_mark,"test coverage"); } |
_initialize_mark函数调用add_com()为gdb添加新的命令。covertest对应的处理函数为cover_command()。其中class_breakpoint是一个枚举变量,表示命令covertest属于断点类的命令。当用户键入help breakpoint后,就能看到命令covertest以及对它的说明,即add_com()的第四个参数”test coverage”。
选择合适的单步命令
GDB提供了几种不同的单步调试命令:step,stepi,next和nexti。
首先attach到setmark时fork的新进程,该进程ID已经保存在全局变量org_pid中。直接调用gdb函数attach_command()完成attach工作。
我们选择step命令来单步执行程序。因为该命令遇到子函数能够进入子函数内部。step命令不会进入动态链接库函数,比如printf。因为没有debug信息。这种特性非常符合代码覆盖率测试的要求。用户使用代码覆盖率测试工具只希望了解自己编写的代码的覆盖率情况。而不需要了解第三方库函数以及系统库函数的覆盖率.比如下面的代码片段:
void main(){ a = 10; printf(“a is %d\n”,a); } |
运行该程序的代码覆盖率显然为100%。但是printf()函数本身非常复杂,用户并不希望了解printf()的覆盖率。该函数非常复杂,显然上述调用不可能百分百地覆盖printf()。如果单步进入printf(),则最终的测试覆盖率结果就包含了对printf的测试,其结果就不会是100%了。
利用gdb这个特性可以自动区分第三方库函数和用户自己编写的函数,这使得代码覆盖率测试的工作更加简单了。
记录单步执行的代码长度
Gdb内部step命令相应的执行函数为:
static void step_1 (int skip_subroutines, int single_inst, char *count_string) |
为了让被调试程序单步执行,可以直接调用step_1(0,0,”1”)。该函数执行结束,目标进程就单步运行了一次,因此我们必须在此时记录下这次单步所执行的机器指令的长度。
Gdb内部函数find_pc_line_pc_range为我们完成了计算单步代码长度的工作。每次调用step_1命令时,gdb都会调用find_pc_line_pc_ragne()函数得到一条C语言语句实际对应的机器代码的起始地址和结束地址。这两个值在gdb中分别存放在step_range_start和step_range_end两个全局变量中。我们只需将两个值相减就可以得到这次单步执行所运行过的机器指令的长度。
求总的代码长度
我们把ELF文件中text段的长度作为总的代码长度。ELF中还有一些段包含了可执行代码,但是我们将他们剔除了。理由是这些段中的代码都不是用户关心的代码。比如.init段和.fini段。这些段是编译器自动生成的。.init的执行在main()函数之前,.fini段代码的执行在exit()函数之后。而我们执行单步函数是从main()之后开始,到exit()之前结束,因此在统计总代码长度时将这两个段的长度剔除。
Gdb将可执行代码的段信息都放在current_target.to_sections中。Current_target是gdb中非常重要的一个数据结构,代表了被调试的目标。其中to_sections域存放了被调试程序ELF文件中所有section的信息。它的类型为struct section_table:
Gdb将可执行代码的段信息都放在current_target.to_sections中。Current_target是gdb中非常重要的一个数据结构,代表了被调试的目标。其中to_sections域存放了被调试程序ELF文件中所有section的信息。它的类型为struct section_table:
struct section_table { CORE_ADDR addr; /* Lowest address in section */ CORE_ADDR endaddr; /* 1+highest address in section */ sec_ptr the_bfd_section; bfd *bfd; /* BFD file pointer */ }; |
遍历to_sections,找到section name为”.text”的段,用endaddr减去addr就得到了该段的长度。
记住曾经走过的路
多数程序都有分支判断和循环结构。因此covertest必须记住曾经运行过的代码,当再次运行到这些代码时,不应该重复记录。比如下例:
int main(){ int i; for(i=0;i<10;i++) foo(); } |
foo函数被调用了10次,但是在计算代码覆盖率时,它只应该被计算一次。
为了记住程序过去走过的路,我们采用了bitmap数据结构。用指令地址作为索引。当某指令地址被记录时,就将相应的bitmap设置为1。当下次再遇到该指令地址时,由于bimap已经为一,我们就知道该指令在曾经走过的路径上,不需要再记录了。
Prologue统计
为了实现函数调用,编译器会在每个子函数头部加入prologue。Gdb执行step命令进入子函数时,会跳过prologue,将断点设在prologue后的第一条指令上。比如下例:
void foo() { int a; a=10; } |
编译后的汇编为:
00000000 <_fooh>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 04 sub $0x4,%esp 6: c7 45 fc 0a 00 00 00 movl $0xa,0xfffffffc(%ebp) d: c9 leave e: c3 ret f: 90 nop |
前三句汇编指令都属于prologue,主要作用是为临时变量a开辟stack中的空间。当使用gdb单步进入该函数时,gdb将第4行,即偏移量为6的机器代码作为该函数的起始地址。而前面6个字节的prologue被跳过。在统计代码覆盖率时,必须将prologue也算入被覆盖的代码。为此我们必须记录下被gdb跳过的prologue的长度。
对于x86平台,gdb对应prologue的处理在函数i386_skip_prologue()中。我们在该函数中增加了一个全局变量skipped_proglogue_len,记录被跳过的prologue的长度。
结论
使用covertest命令使用非常简单,将被测试程序用gdb打开。首先在main函数处设置断点。然后直接调用covertest命令。下面是一个用covertest进行代码覆盖率测试的例子。
被测程序一:
//test1.c void foo() { printf(“test\n”); } int main(void) { int a = 1; if (a ==1) foo(); } |
被测程序二:
//test2.c void foo() { printf(“test\n”); } int main(void) { int a = 0; if (a ==1) foo(); } |
很显然test1的覆盖率应该为100%,而test2则不到100%。分别编译他们:
$gcc –g –o test1 test1.c $gcc –g –o test2 test.c |
用gdb打开test1
$gdb test1 (gdb) b main (gdb) covertest test coverage rate: 100% (gdb) |
同样的方法测试test2得到覆盖率为94%
结论
Gdb本身拥有强大的符号处理和进程控制能力,合理地利用gdb的这些能力,我们还能开发出更多的功能。比如稍微修改一下covertestt命令就可以实现程序执行流程的log功能。测试人员提交defect报告时,如果能将错误产生的执行路径也一起提交对于开发工程师将非常有帮助。
(责任编辑:A6)