本博客(http://blog.csdn.net/livelylittlefish)贴出作者(阿波)相关研究、学习内容所做的笔记,欢迎广大朋友指正!
Content
0. 序
1. 如何编译
1.1 未加入覆盖率测试选项
1.2 加入覆盖率测试选项
1.3 分析
2. 未加入覆盖率测试选项的汇编代码分析
3. 加入覆盖率测试选项的汇编代码分析
3.1 计数桩代码分析
3.2 构造函数桩代码分析
3.3 数据结构分析
3.4 构造函数桩代码小结
4. 说明
5. 小结
0.序
在"Linux平台代码覆盖率测试-GCC插桩基本概念和原理分析"一文中,我们已经知道,GCC插桩乃汇编级的插桩,那么,本文仍然以test.c为例,来分析加入覆盖率测试选项"-fprofile-arcs -ftest-coverage"前后,即插桩前后汇编代码的变化。本文所用gcc版本为gcc-4.1.2。test.c代码如下。
/** * filename: test.c */ #include <stdio.h> int main (void) { int i, total; total = 0; for (i = 0; i < 10; i++) total += i; if (total != 45) printf ("Failure\n"); else printf ("Success\n"); return 0; }
1. 如何编译
1.1未加入覆盖率测试选项
# cpp test.c-o test.i //预处理:生成test.i文件,或者"cpp test.c > test.i"
或者
# gcc -E test.c -o test.i
# gcc-S test.i //编译:生成test.s文件(未加入覆盖率测试选项)
# as -o test.o test.s //汇编:生成test.o文件,或者"gcc -c test.s -o test.o"
# gcc -o test test.o //链接:生成可执行文件test
以上过程可参考http://blog.csdn.net/livelylittlefish/archive/2009/12/30/5109300.aspx。
查看test.o文件中的符号
# nm test.o
00000000 T main
U puts
1.2加入覆盖率测试选项
# cpp test.c-o test.i //预处理:生成test.i文件
# gcc-fprofile-arcs -ftest-coverage-S test.i //编译:生成test.s文件(加入覆盖率测试选项)
# as -o test.o test.s //汇编:生成test.o文件
# gcc -o test test.o //链接:生成可执行文件test
查看test.o文件中的符号
# nm test.o
000000eb t _GLOBAL__I_0_main
U __gcov_init
U __gcov_merge_add
00000000 T main
U puts
1.3分析
从上面nm命令的结果可以看出,加入覆盖率测试选项后的test.o文件,多了3个符号,如上。其中,_GLOBAL__I_0_main就是插入的部分桩代码。section2和section3将对比分析插桩前后汇编代码的变化,section3重点分析插入的桩代码。
2.未加入覆盖率测试选项的汇编代码分析
采用"# gcc-S test.i"命令得到的test.s汇编代码如下。#后面的注释为笔者所加。
.file "test.c" .section .rodata .LC0: .string "Failure" .LC1: .string "Success" .text .globl main .type main, @function main: leal 4(%esp), %ecx #这几句就是保护现场 andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $20, %esp movl $0, -8(%ebp) #初始化total=0,total的值在-8(%ebp)中 movl $0, -12(%ebp) #初始化循环变量i=0,i的值在-12(%ebp)中 jmp .L2 .L3: movl -12(%ebp), %eax #将i的值移到%eax中,即%eax=i addl %eax, -8(%ebp) #将%eax的值加到-8(%ebp),total=total+i addl $1, -12(%ebp) #循环变量加1,即i++ .L2: cmpl $9, -12(%ebp) #比较循环变量i与9的大小 jle .L3 #如果i<=9,跳到.L3,继续累加 cmpl $45, -8(%ebp) #否则,比较total的值与45的大小 je .L5 #若total=45,跳到.L5 movl $.LC0, (%esp) #否total的值不为45,则将$.LC0放入%esp call puts #输出Failure jmp .L7 #跳到.L7 .L5: movl $.LC1, (%esp) #将$.LC1放入%esp call puts #输出Success .L7: movl $0, %eax #返回值0放入%eax addl $20, %esp #这几句恢复现场 popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main .ident "GCC: (GNU) 4.1.2 20070925 (Red Hat 4.1.2-33)" .section .note.GNU-stack,"",@progbits
注:$9表示常量9,即立即数(Immediate Operand)。-8(%ebp)即为total,-12(%ebp)即是循环变量i。
3.加入覆盖率测试选项的汇编代码分析
采用"# gcc-fprofile-arcs -ftest-coverage-S test.i"命令得到的test.s汇编代码如下。前面的蓝色部分及后面的.LC2, .LC3, .LPBX0, _GLOBAL__I_0_main等均为插入的桩代码。#后面的注释为笔者所加。
.file "test.c" .section .rodata .LC0: .string "Failure" .LC1: .string "Success" .text .globl main .type main, @function main: leal 4(%esp), %ecx #这几句就是保护现场 andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $20, %esp movl $0, -8(%ebp) #初始化total=0,total的值在-8(%ebp)中 movl $0, -12(%ebp) #初始化循环变量i=0,i的值在-12(%ebp)中 jmp .L2 .L3: #以下这几句就是插入的桩代码 movl .LPBX1, %eax #将.LPBX1移到%eax,即%eax=.LPBX1 movl .LPBX1+4, %edx #edx=.LPBX1+4 addl $1, %eax #eax=%eax+1 adcl $0, %edx #edx=%edx+0 movl %eax, .LPBX1 #将%eax移回.LPBX1 movl %edx, .LPBX1+4 #将%edx移回.LPBX1+4 movl -12(%ebp), %eax #将i的值移到%eax中,即%eax=i addl %eax, -8(%ebp) #将%eax的值加到-8(%ebp),total=total+i addl $1, -12(%ebp) #循环变量加1,即i++ .L2: cmpl $9, -12(%ebp) #比较循环变量i与9的大小 jle .L3 #如果i<=9,跳到.L3,继续累加 cmpl $45, -8(%ebp) #否则,比较total的值与45的大小 je .L5 #若total=45,跳到.L5 #以下也为桩代码 movl .LPBX1+8, %eax #eax=.LPBX1+8 movl .LPBX1+12, %edx #edx=.LPBX1+12 addl $1, %eax #eax=%eax+1 adcl $0, %edx #edx=%edx+0 movl %eax, .LPBX1+8 #将%eax移回.LPBX1+8 movl %edx, .LPBX1+12 #将%eax移回.LPBX1+12 movl $.LC0, (%esp) #否total的值不为45,则将$.LC0放入%esp call puts #输出Failure #以下也为桩代码,功能同上,不再解释 movl .LPBX1+24, %eax movl .LPBX1+28, %edx addl $1, %eax adcl $0, %edx movl %eax, .LPBX1+24 movl %edx, .LPBX1+28 jmp .L7 #跳到.L7 .L5: #以下也为桩代码,功能同上,不再解释 movl .LPBX1+16, %eax movl .LPBX1+20, %edx addl $1, %eax adcl $0, %edx movl %eax, .LPBX1+16 movl %edx, .LPBX1+20 movl $.LC1, (%esp) #将$.LC1放入%esp call puts #输出Success #以下也为桩代码,功能同上,不再解释 movl .LPBX1+32, %eax movl .LPBX1+36, %edx addl $1, %eax adcl $0, %edx movl %eax, .LPBX1+32 movl %edx, .LPBX1+36 .L7: movl $0, %eax #返回值0放入%eax addl $20, %esp #这几句回复现场 popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main #以下部分均是加入coverage选项后编译器加入的桩代码 .local .LPBX1 .comm .LPBX1,40,32 .section .rodata #只读section .align 4 .LC2: #文件名常量,只读 .string "/home/zubo/gcc/test/test.gcda" .data #data数据段 .align 4 .LC3: .long 3 #ident=3 .long -345659544 #即checksum=0xeb65a768 .long 5 #counters .align 32 .type .LPBX0, @object #.LPBX0是一个对象 .size .LPBX0, 52 #.LPBX0大小为52字节 .LPBX0: #结构的起始地址,即结构名,该结构即为gcov_info结构 .long 875573616 #即version=0x34303170,即版本为4.1p .long 0 #即next指针,为0 .long -979544300 #即stamp=0xc59d5714 .long .LC2 #filename,值为.LC2的常量 .long 1 #n_functions=1 .long .LC3 #functions指针,指向.LC3 .long 1 #ctr_mask=1 .long 5 #以下3个字段构成gcov_ctr_info结构,该字段num=5,即counter的个数 .long .LPBX1 #values指针,指向.LPBX1,即5个counter的内容在.LPBX1结构中 .long __gcov_merge_add #merge指针,指向__gcov_merge_add函数 .zero 12 #应该是12个0 .text #text代码段 .type _GLOBAL__I_0_main, @function #类型是function _GLOBAL__I_0_main: #以下是函数体 pushl %ebp movl %esp, %ebp subl $8, %esp movl $.LPBX0, (%esp) #将$.LPBX0,即.LPBX0的地址,存入%esp所指单元 #实际上是为下面调用__gcov_init准备参数,即gcov_info结构指针 call __gcov_init #调用__gcov_init leave ret .size _GLOBAL__I_0_main, .-_GLOBAL__I_0_main .section .ctors,"aw",@progbits #该函数位于ctors段 .align 4 .long _GLOBAL__I_0_main .align 4 .long _GLOBAL__I_0_main .ident "GCC: (GNU) 4.1.2 20070925 (Red Hat 4.1.2-33)" .section .note.GNU-stack,"",@progbits
3.1计数桩代码分析
共插入了6段桩代码,前5段桩代码很容易理解。实际上就是一个计数器,只要每次执行到相关代码,即会让该计数器加1。我们以第一处桩代码为例,如下。
movl .LPBX1, %eax #将.LPBX1移到%eax,即%eax=.LPBX1 movl .LPBX1+4, %edx #edx=.LPBX1+4 addl $1, %eax #eax=%eax+1 adcl $0, %edx #edx=%edx+0 movl %eax, .LPBX1 #将%eax移回.LPBX1 movl %edx, .LPBX1+4 #将%edx移回.LPBX1+4
从该段汇编代码可以看出,这段代码要完成的功能实际上就是让这个计数器加1,但该计数器是谁?
——就是.LPBX1和.LPBX1+4组成的8个字节的长长整数。而前5处桩代码,实际上就是对一个有5个长长整数元素的静态数组的
为什么是静态数组?
.local .LPBX1 .comm .LPBX1,40,32 .section .rodata #只读section .align 4
从.LPBX1的section属性可以看出该数组应该是rodata,即只读,其中的40应该就是其长度,即40字节。如下便是LPBX1数组,大小共40字节,以4字节方式对齐。
+0 +4 +8 +12 +16 +20 +24 +28 +32 +36
10 |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
0 |
代码运行后,该数组的值就记录了桩代码被执行的次数,也即其后的代码块被执行的次数,如上所示。
3.2构造函数桩代码分析
插入的第6段桩代码,先不管他的功能,先分析一下以下代码。
.text #text代码段 .type _GLOBAL__I_0_main, @function #类型是function _GLOBAL__I_0_main: #以下是函数体 pushl %ebp movl %esp, %ebp subl $8, %esp movl $.LPBX0, (%esp) #将$.LPBX0,即.LPBX0的地址,存入%esp所指单元 #实际上是为下面调用__gcov_init准备参数,即gcov_info结构指针 call __gcov_init #调用__gcov_init leave ret
可以看出,这是一个函数,函数名为_GLOBAL__I_0_main,该函数的主要目的是调用__gcov_init函数,调用参数就是.LPBX0结构。
将可执行文件test通过objdump命令dump出来,查看该符号,也一目了然。
0804891b <_GLOBAL__I_0_main>: 804891b: 55 push %ebp 804891c: 89 e5 mov %esp,%ebp 804891e: 83 ec 08 sub $0x8,%esp //将$.LPBX0,即.LPBX0的地址,存入%esp所指单元 //实际上是为下面调用__gcov_init准备参数,即gcov_info结构指针 //此处gcov_info的地址即为0x804b7a0,当然这是一个虚拟地址 8048921: c7 04 24 a0 b7 04 08 movl $0x804b7a0,(%esp) 8048928: e8 93 01 00 00 call 8048ac0 <__gcov_init> //调用__gcov_init 804892d: c9 leave 804892e: c3 ret 804892f: 90 nop
接下来,看看__gcov_init函数,定义如下。
void __gcov_init (struct gcov_info *info) { if (! info- >version) return; if (gcov_version (info, info->version, 0)) { const char *ptr = info- >filename; gcov_unsigned_t crc32 = gcov_crc32; size_t filename_length = strlen(info- >filename); /* Refresh the longest file name information */ if (filename_length > gcov_max_filename) gcov_max_filename = filename_length; do { unsigned ix; gcov_unsigned_t value = *ptr << 24; for (ix = 8; ix-- ; value <<= 1) { gcov_unsigned_t feedback; feedback = (value ^ crc32) & 0x80000000 ? 0x04c11db7 : 0; crc32 <<= 1; crc32 ^= feedback; } }while (*ptr++); gcov_crc32 = crc32; if (! gcov_list) atexit (gcov_exit); info- >next = gcov_list; /* Insert info into list gcov_list */ gcov_list = info; /* gcov_list is the list head */ } info->version = 0; }
由此,我们得到两个结论:
(1).LPBX0结构就是gcov_info结构,二者相同。
(2) __gcov_init的功能:将.LPBX0结构,即gcov_info结构,串成一个链表,该链表指针就是gcov_list。
我们先看看这些数据结构。
3.3数据结构分析
.LPBX0结构即为gcov_info结构,定义如下。
/* Type of function used to merge counters. */ typedef void (*gcov_merge_fn) (gcov_type *, gcov_unsigned_t); /* Information about counters. */ struct gcov_ctr_info { gcov_unsigned_t num; /* number of counters. */ gcov_type *values; /* their values. */ gcov_merge_fn merge; /* The function used to merge them. */ }; /* Information about a single object file. */ struct gcov_info { gcov_unsigned_t version; /* expected version number */ struct gcov_info *next; /* link to next, used by libgcov */ gcov_unsigned_t stamp; /* uniquifying time stamp */ const char *filename; /* output file name */ unsigned n_functions; /* number of functions */ const struct gcov_fn_info *functions; /* table of functions */ unsigned ctr_mask; /* mask of counters instrumented. */ struct gcov_ctr_info counts[0]; /* count data. The number of bits set in the ctr_mask field determines how big this array is. */ };
对应于上述代码中的解释,便一目了然。此处再重复一下对该结构的解释。
.align 32 .type .LPBX0, @object #.LPBX0是一个对象 .size .LPBX0, 52 #.LPBX0大小为52字节 .LPBX0: #结构的起始地址,即结构名,该结构即为gcov_info结构 .long 875573616 #即version=0x34303170,即版本为4.1p .long 0 #即next指针,为0,next为空 .long -979544300 #即stamp=0xc59d5714 .long .LC2 #filename,值为.LC2的常量 .long 1 #n_functions=1,1个函数 .long .LC3 #functions指针,指向.LC3 .long 1 #ctr_mask=1 .long 5 #以下3个字段构成gcov_ctr_info结构,该字段num=5,即counter的个数 .long .LPBX1 #values指针,指向.LPBX1,即5个counter的内容在.LPBX1结构中 .long __gcov_merge_add #merge指针,指向__gcov_merge_add函数 .zero 12 #应该是12个0
上述的.LC2即为文件名,如下。
.section .rodata #只读section .align 4 .LC2: #文件名常量,只读 .string "/home/zubo/gcc/test/test.gcda"
然后就是functions结构,1个函数,函数结构就是.LC3的内容。
.LC3: .long 3 #ident=3 .long -345659544 #即checksum=0xeb65a768 .long 5 #counters
其对应的结构为gcov_fn_info,定义如下。
/ * Information about a single function. This uses the trailing array idiom. The number of counters is determined from the counter_mask in gcov_info. We hold an array of function info, so have to explicitly calculate the correct array stride. */ struct gcov_fn_info { gcov_unsigned_t ident; /* unique ident of function */ gcov_unsigned_t checksum; /* function checksum */ unsigned n_ctrs[0]; /* instrumented counters */ };
3.4构造函数桩代码小结
gcov_init函数中的gcov_list是一个全局指针,指向gcov_info结构的链表,定义如下。
/ * Chain of per- object gcov structures. */ staticstructgcov_info *gcov_list;
因此,被测文件在进入main之前,所有文件的.LPBX0结构就被组织成一个链表,链表头就是gcov_list。被测程序运行完之后,在__gcov_init()中通过atexit()注册的函数gcov_exit()就被调用。该函数将从gcov_list的第一个.LPBX0结构开始,为每个被测文件生成一个.gcda文件。.gcda文件的主要内容就是.LPBX0结构的内容。
至此,我们可以做这样的总结:将.LPBX0结构串成链表的目的是在被测程序运行结束时统一写入计数信息到.gcda文件。
因此,为了将LPBX0结构链成一条链,GCC要为每个被测试源文件中插入一个构造函数_GLOBAL__I_0_main的桩代码,该函数名根据当前被测文件中的第一个全局函数的名字生成,其中main即为test.c中的第一个全局函数名,防止重名。
而之所以称为构造函数,是因为该函数类似C++的构造函数,在调用main函数之前就会被调用。
4.说明
本文参考文献中实际分析的gcc代码应该是gcc-2.95版本,而本文分析的gcc代码是gcc-4.1.2版本。可以发现这两个版本间变化非常非常大。
gcc-2.95版本中有__bb_init_func()函数和__bb_exit_func()函数,并且其中的结构为bb结构。
但在gcc-4.1.2版本中,就变为__gcov_init()函数和gcov_exit()函数,对应的结构为gcov_info结构。
5.小结
本文详细叙述了Linux平台代码覆盖率测试插桩前后汇编代码的变化及分析,对于分析gcc插桩、gcov原理有很大的帮助。
Reference
费训,罗蕾.利用GNU工具实现汇编程序覆盖测试,计算机应用, 24卷, 2004.
吴康.面向多语言混合编程的嵌入式测试软件设计与实现(硕士论文).电子科技大学, 2007.
http://gcc.parentingamerica.com/releases/gcc-2.95.3
http://gcc.parentingamerica.com/releases/gcc-4.1.2
Technorati 标签: 覆盖率测试,GCC,gcov,gcov-dump