记录单步执行的代码长度Gdb内部step命令相应的执行函数为:
1
2
3
| 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:
1
2
3
4
5
6
7
| 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必须记住曾经运行过的代码,当再次运行到这些代码时,不应该重复记录。比如下例:
1
2
3
4
5
| int main(){
int i;
for(i=0;i<10;i++)
foo();
}
|
foo函数被调用了10次,但是在计算代码覆盖率时,它只应该被计算一次。
为了记住程序过去走过的路,我们采用了bitmap数据结构。用指令地址作为索引。当某指令地址被记录时,就将相应的bitmap设置为1。当下次再遇到该指令地址时,由于bimap已经为一,我们就知道该指令在曾经走过的路径上,不需要再记录了。
Prologue统计为了实现函数调用,编译器会在每个子函数头部加入prologue。Gdb执行step命令进入子函数时,会跳过prologue,将断点设在prologue后的第一条指令上。比如下例:
1
2
3
4
5
| void foo()
{
int a;
a=10;
}
|
编译后的汇编为:
1
2
3
4
5
6
7
8
| 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进行代码覆盖率测试的例子。
被测程序一:
1
2
3
4
5
6
7
8
9
| //test1.c
void foo()
{
printf(“test\n”);
}
int main(void) {
int a = 1;
if (a ==1) foo();
}
|
被测程序二:
1
2
3
4
5
6
7
8
9
| //test2.c
void foo()
{
printf(“test\n”);
}
int main(void) {
int a = 0;
if (a ==1) foo();
}
|
很显然test1的覆盖率应该为100%,而test2则不到100%。分别编译他们:
1
2
| $gcc –g –o test1 test1.c
$gcc –g –o test2 test.c
|
用gdb打开test1
1
2
3
4
5
6
| $gdb test1
(gdb) b main
(gdb) covertest
test
coverage rate: 100%
(gdb)
|
同样的方法测试test2得到覆盖率为94%
结论Gdb本身拥有强大的符号处理和进程控制能力,合理地利用gdb的这些能力,我们还能开发出更多的功能。比如稍微修改一下covertestt命令就可以实现程序执行流程的log功能。测试人员提交defect报告时,如果能将错误产生的执行路径也一起提交对于开发工程师将非常有帮助。 |