Content0. 序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 int main (void){ int i, total; total = 0; for (i = 0; i < 10; i++) total += i; if (total != 45) printf ("Failure
"); else printf ("Success
"); 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://www.linuxidc.com/Linux/2011-05/36542.htm。 查看test.o文件中的符号# nm test.o00000000 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.o000000eb t _GLOBAL__I_0_main U __gcov_init U __gcov_merge_add00000000 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, @functionmain: 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, @functionmain: 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