本博客(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代码如下。
-
-
-
- #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
代码运行后,该数组的值就记录了桩代码被执行的次数,也即其后的代码块被执行的次数,如上所示。
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);
-
-
- 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;
- gcov_list = info;
- }
- info->version = 0;
- }
由此,我们得到两个结论:
(1).LPBX0结构就是gcov_info结构,二者相同。
(2) __gcov_init的功能:将.LPBX0结构,即gcov_info结构,串成一个链表,该链表指针就是gcov_list。
我们先看看这些数据结构。
3.3数据结构分析
.LPBX0结构即为gcov_info结构,定义如下。
-
- typedef void (*gcov_merge_fn) (gcov_type *, gcov_unsigned_t);
-
-
- struct gcov_ctr_info
- {
- gcov_unsigned_t num;
- gcov_type *values;
- gcov_merge_fn merge;
- };
-
-
- struct gcov_info
- {
- gcov_unsigned_t version;
- struct gcov_info *next;
-
- gcov_unsigned_t stamp;
- const char *filename;
-
- unsigned n_functions;
- const struct gcov_fn_info *functions;
-
- unsigned ctr_mask;
- struct gcov_ctr_info counts[0];
-
-
- };
对应于上述代码中的解释,便一目了然。此处再重复一下对该结构的解释。
- .align 32
- .type .LPBX0, @object #.LPBX0是一个对象
- .size .LPBX0, 52 #.LPBX0大小为52字节
- X0: #结构的起始地址,即结构名,该结构即为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;
- gcov_unsigned_t checksum;
- unsigned n_ctrs[0];
- };
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