Linux平台代码覆盖率测试-GCC插桩前后汇编代码对比分析
Linux平台代码覆盖率测试-GCC插桩前后汇编代码对比分析
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
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
以上过程可参考。
查看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
|
评论暂时关闭