GCC Coverage代码分析-GCC插桩前后汇编代码对比分析

本博客(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.2test.c代码如下。

[cpp]  view plain copy
  1. /** 
  2.  * filename: test.c 
  3.  */  
  4. #include <stdio.h>  
  5.    
  6. int main (void)  
  7. {  
  8.     int i, total;  
  9.    
  10.     total = 0;  
  11.    
  12.     for (i = 0; i < 10; i++)  
  13.         total += i;  
  14.   
  15.     if (total != 45)  
  16.         printf ("Failure\n");  
  17.     else  
  18.         printf ("Success\n");  
  19.     return 0;  
  20. }  

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就是插入的部分桩代码。section2section3将对比分析插桩前后汇编代码的变化,section3重点分析插入的桩代码。

 

2.未加入覆盖率测试选项的汇编代码分析

 

采用"# gcc-S test.i"命令得到的test.s汇编代码如下。#后面的注释为笔者所加。

[plain]  view plain copy
  1.     .file    "test.c"  
  2.     .section    .rodata  
  3. .LC0:  
  4.     .string    "Failure"  
  5. .LC1:  
  6.     .string    "Success"  
  7.     .text  
  8. .globl main  
  9.     .type    main, @function  
  10. main:  
  11.     leal    4(%esp), %ecx    #这几句就是保护现场  
  12.     andl    $-16, %esp  
  13.     pushl    -4(%ecx)  
  14.     pushl    %ebp  
  15.     movl    %esp, %ebp  
  16.     pushl    %ecx  
  17.     subl    $20, %esp  
  18.   
  19.     movl    $0, -8(%ebp)     #初始化total=0,total的值在-8(%ebp)中  
  20.     movl    $0, -12(%ebp)    #初始化循环变量i=0,i的值在-12(%ebp)中  
  21.     jmp    .L2  
  22. .L3:  
  23.     movl    -12(%ebp), %eax  #将i的值移到%eax中,即%eax=i  
  24.     addl    %eax, -8(%ebp)   #将%eax的值加到-8(%ebp),total=total+i  
  25.     addl    $1, -12(%ebp)    #循环变量加1,即i++  
  26. .L2:  
  27.     cmpl    $9, -12(%ebp)    #比较循环变量i与9的大小  
  28.     jle    .L3               #如果i<=9,跳到.L3,继续累加  
  29.     cmpl    $45, -8(%ebp)    #否则,比较total的值与45的大小  
  30.     je     .L5               #若total=45,跳到.L5  
  31.     movl    $.LC0, (%esp)    #否total的值不为45,则将$.LC0放入%esp  
  32.     call    puts             #输出Failure  
  33.     jmp    .L7               #跳到.L7  
  34. .L5:  
  35.     movl    $.LC1, (%esp)    #将$.LC1放入%esp  
  36.     call    puts             #输出Success  
  37. .L7:  
  38.     movl    $0, %eax         #返回值0放入%eax  
  39.   
  40.     addl    $20, %esp        #这几句恢复现场  
  41.     popl    %ecx  
  42.     popl    %ebp  
  43.     leal    -4(%ecx), %esp  
  44.     ret  
  45.   
  46.     .size    main, .-main  
  47.     .ident    "GCC: (GNU) 4.1.2 20070925 (Red Hat 4.1.2-33)"  
  48.     .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等均为插入的桩代码。#后面的注释为笔者所加。

[plain]  view plain copy
  1.     .file      "test.c"  
  2.     .section   .rodata  
  3. .LC0:  
  4.     .string    "Failure"  
  5. .LC1:  
  6.     .string    "Success"  
  7.     .text  
  8. .globl main  
  9.     .type    main, @function  
  10. main:  
  11.     leal    4(%esp), %ecx    #这几句就是保护现场  
  12.     andl    $-16, %esp  
  13.     pushl    -4(%ecx)  
  14.     pushl    %ebp  
  15.     movl    %esp, %ebp  
  16.     pushl    %ecx  
  17.     subl    $20, %esp  
  18.   
  19.     movl    $0, -8(%ebp)     #初始化total=0,total的值在-8(%ebp)中  
  20.     movl    $0, -12(%ebp)    #初始化循环变量i=0,i的值在-12(%ebp)中  
  21.     jmp    .L2  
  22.   
  23. .L3:                         #以下这几句就是插入的桩代码  
  24.     movl    .LPBX1, %eax     #将.LPBX1移到%eax,即%eax=.LPBX1  
  25.     movl    .LPBX1+4, %edx   #edx=.LPBX1+4  
  26.     addl    $1, %eax         #eax=%eax+1  
  27.     adcl    $0, %edx         #edx=%edx+0  
  28.     movl    %eax, .LPBX1     #将%eax移回.LPBX1  
  29.     movl    %edx, .LPBX1+4   #将%edx移回.LPBX1+4  
  30.   
  31.     movl    -12(%ebp), %eax  #将i的值移到%eax中,即%eax=i  
  32.     addl    %eax, -8(%ebp)   #将%eax的值加到-8(%ebp),total=total+i  
  33.     addl    $1, -12(%ebp)    #循环变量加1,即i++  
  34.   
  35. .L2:  
  36.     cmpl    $9, -12(%ebp)    #比较循环变量i与9的大小  
  37.     jle    .L3               #如果i<=9,跳到.L3,继续累加  
  38.     cmpl    $45, -8(%ebp)    #否则,比较total的值与45的大小  
  39.     je     .L5               #若total=45,跳到.L5  
  40.   
  41.     #以下也为桩代码  
  42.     movl    .LPBX1+8, %eax   #eax=.LPBX1+8  
  43.     movl    .LPBX1+12, %edx  #edx=.LPBX1+12  
  44.     addl    $1, %eax         #eax=%eax+1  
  45.     adcl    $0, %edx         #edx=%edx+0  
  46.     movl    %eax, .LPBX1+8   #将%eax移回.LPBX1+8  
  47.     movl    %edx, .LPBX1+12  #将%eax移回.LPBX1+12  
  48.   
  49.     movl    $.LC0, (%esp)    #否total的值不为45,则将$.LC0放入%esp  
  50.     call    puts             #输出Failure  
  51.   
  52.     #以下也为桩代码,功能同上,不再解释  
  53.     movl    .LPBX1+24, %eax  
  54.     movl    .LPBX1+28, %edx  
  55.     addl    $1, %eax  
  56.     adcl    $0, %edx  
  57.     movl    %eax, .LPBX1+24  
  58.     movl    %edx, .LPBX1+28  
  59.   
  60.     jmp    .L7               #跳到.L7  
  61.   
  62. .L5:  
  63.     #以下也为桩代码,功能同上,不再解释  
  64.     movl    .LPBX1+16, %eax  
  65.     movl    .LPBX1+20, %edx  
  66.     addl    $1, %eax  
  67.     adcl    $0, %edx  
  68.     movl    %eax, .LPBX1+16  
  69.     movl    %edx, .LPBX1+20  
  70.   
  71.     movl    $.LC1, (%esp)    #将$.LC1放入%esp  
  72.     call    puts             #输出Success  
  73.   
  74.     #以下也为桩代码,功能同上,不再解释  
  75.     movl    .LPBX1+32, %eax  
  76.     movl    .LPBX1+36, %edx  
  77.     addl    $1, %eax  
  78.     adcl    $0, %edx  
  79.     movl    %eax, .LPBX1+32  
  80.     movl    %edx, .LPBX1+36  
  81.   
  82. .L7:  
  83.     movl    $0, %eax         #返回值0放入%eax  
  84.     addl    $20, %esp        #这几句回复现场  
  85.     popl    %ecx  
  86.     popl    %ebp  
  87.     leal    -4(%ecx), %esp  
  88.     ret  
  89.   
  90.     .size    main, .-main  
  91.   
  92.     #以下部分均是加入coverage选项后编译器加入的桩代码  
  93.   
  94.     .local   .LPBX1  
  95.     .comm    .LPBX1,40,32  
  96.   
  97.     .section .rodata      #只读section  
  98.     .align   4  
  99. .LC2:                     #文件名常量,只读  
  100.     .string  "/home/zubo/gcc/test/test.gcda"  
  101.   
  102.     .data                 #data数据段  
  103.     .align   4  
  104. .LC3:  
  105.     .long    3            #ident=3  
  106.     .long    -345659544   #即checksum=0xeb65a768  
  107.     .long    5            #counters  
  108.   
  109.     .align   32  
  110.     .type    .LPBX0, @object #.LPBX0是一个对象  
  111.     .size    .LPBX0, 52   #.LPBX0大小为52字节  
  112. .LPBX0:                   #结构的起始地址,即结构名,该结构即为gcov_info结构  
  113.     .long    875573616    #即version=0x34303170,即版本为4.1p  
  114.     .long    0            #即next指针,为0  
  115.     .long    -979544300   #即stamp=0xc59d5714  
  116.     .long    .LC2         #filename,值为.LC2的常量  
  117.     .long    1            #n_functions=1  
  118.     .long    .LC3         #functions指针,指向.LC3  
  119.     .long    1            #ctr_mask=1  
  120.     .long    5            #以下3个字段构成gcov_ctr_info结构,该字段num=5,即counter的个数  
  121.     .long    .LPBX1       #values指针,指向.LPBX1,即5个counter的内容在.LPBX1结构中  
  122.     .long    __gcov_merge_add #merge指针,指向__gcov_merge_add函数  
  123.     .zero    12           #应该是12个0  
  124.   
  125.     .text                                  #text代码段  
  126.     .type    _GLOBAL__I_0_main, @function  #类型是function  
  127. _GLOBAL__I_0_main:                         #以下是函数体  
  128.     pushl    %ebp  
  129.     movl     %esp, %ebp  
  130.     subl     $8, %esp  
  131.     movl     $.LPBX0, (%esp)   #将$.LPBX0,即.LPBX0的地址,存入%esp所指单元  
  132.                                #实际上是为下面调用__gcov_init准备参数,即gcov_info结构指针  
  133.     call     __gcov_init       #调用__gcov_init  
  134.     leave  
  135.     ret  
  136.   
  137.     .size    _GLOBAL__I_0_main, .-_GLOBAL__I_0_main  
  138.     .section    .ctors,"aw",@progbits      #该函数位于ctors段  
  139.     .align 4  
  140.     .long    _GLOBAL__I_0_main  
  141.     .align 4  
  142.     .long    _GLOBAL__I_0_main  
  143.    
  144.    .ident    "GCC: (GNU) 4.1.2 20070925 (Red Hat 4.1.2-33)"  
  145.    .section    .note.GNU-stack,"",@progbits  

3.1计数桩代码分析

共插入了6段桩代码,前5段桩代码很容易理解。实际上就是一个计数器,只要每次执行到相关代码,即会让该计数器加1。我们以第一处桩代码为例,如下。

[plain]  view plain copy
  1. movl    .LPBX1, %eax     #将.LPBX1移到%eax,即%eax=.LPBX1  
  2. movl    .LPBX1+4, %edx   #edx=.LPBX1+4  
  3. addl    $1, %eax         #eax=%eax+1  
  4. adcl    $0, %edx         #edx=%edx+0  
  5. movl    %eax, .LPBX1     #将%eax移回.LPBX1  
  6. movl    %edx, .LPBX1+4   #将%edx移回.LPBX1+4  

从该段汇编代码可以看出,这段代码要完成的功能实际上就是让这个计数器加1,但该计数器是谁?

——就是.LPBX1.LPBX1+4组成的8个字节的长长整数。而前5处桩代码,实际上就是对一个有5个长长整数元素的静态数组的

为什么是静态数组?

[plain]  view plain copy
  1. .local   .LPBX1  
  2. .comm    .LPBX1,40,32  
  3. .section .rodata      #只读section  
  4. .align   4  

.LPBX1section属性可以看出该数组应该是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段桩代码,先不管他的功能,先分析一下以下代码。

[plain]  view plain copy
  1.     .text                                  #text代码段  
  2.     .type    _GLOBAL__I_0_main, @function  #类型是function  
  3. _GLOBAL__I_0_main:                         #以下是函数体  
  4.     pushl    %ebp  
  5.     movl     %esp, %ebp  
  6.     subl     $8, %esp  
  7.     movl     $.LPBX0, (%esp)   #将$.LPBX0,即.LPBX0的地址,存入%esp所指单元  
  8.                                #实际上是为下面调用__gcov_init准备参数,即gcov_info结构指针  
  9.     call     __gcov_init       #调用__gcov_init  
  10.     leave  
  11.     ret  

可以看出,这是一个函数,函数名为_GLOBAL__I_0_main,该函数的主要目的是调用__gcov_init函数,调用参数就是.LPBX0结构。

将可执行文件test通过objdump命令dump出来,查看该符号,也一目了然。

[plain]  view plain copy
  1. 0804891b <_GLOBAL__I_0_main>:  
  2.  804891b:       55                      push   %ebp  
  3.  804891c:       89 e5                   mov    %esp,%ebp  
  4.  804891e:       83 ec 08                sub    $0x8,%esp  
  5.                                         //将$.LPBX0,即.LPBX0的地址,存入%esp所指单元  
  6.                                         //实际上是为下面调用__gcov_init准备参数,即gcov_info结构指针  
  7.                                         //此处gcov_info的地址即为0x804b7a0,当然这是一个虚拟地址  
  8.  8048921:       c7 04 24 a0 b7 04 08    movl   $0x804b7a0,(%esp)  
  9.  8048928:       e8 93 01 00 00          call   8048ac0 <__gcov_init>    //调用__gcov_init  
  10.  804892d:       c9                      leave    
  11.  804892e:       c3                      ret      
  12.  804892f:       90                      nop          

接下来,看看__gcov_init函数,定义如下。

[cpp]  view plain copy
  1. void __gcov_init (struct gcov_info *info)  
  2. {  
  3.     if (! info- >version)  
  4.         return;  
  5.   
  6.     if (gcov_version (info, info->version, 0))  
  7.     {  
  8.         const char *ptr = info- >filename;  
  9.         gcov_unsigned_t crc32 = gcov_crc32;  
  10.         size_t filename_length = strlen(info- >filename);  
  11.    
  12.         /* Refresh the longest file name information */  
  13.         if (filename_length > gcov_max_filename)  
  14.             gcov_max_filename = filename_length;  
  15.   
  16.         do  
  17.         {  
  18.             unsigned ix;  
  19.             gcov_unsigned_t value = *ptr << 24;  
  20.   
  21.             for (ix = 8; ix-- ; value <<= 1)  
  22.             {  
  23.                 gcov_unsigned_t feedback;  
  24.                 feedback = (value ^ crc32) & 0x80000000 ? 0x04c11db7 : 0;  
  25.                 crc32 <<= 1;  
  26.                 crc32 ^= feedback;  
  27.             }  
  28.         }while (*ptr++);  
  29.    
  30.         gcov_crc32 = crc32;  
  31.   
  32.         if (! gcov_list)  
  33.             atexit (gcov_exit);  
  34.   
  35.         info- >next = gcov_list; /* Insert info into list gcov_list */  
  36.         gcov_list = info;        /* gcov_list is the list head */  
  37.     }  
  38.     info->version = 0;  
  39. }  

由此,我们得到两个结论:

(1).LPBX0结构就是gcov_info结构,二者相同。

(2) __gcov_init的功能:将.LPBX0结构,即gcov_info结构,串成一个链表,该链表指针就是gcov_list 

我们先看看这些数据结构。

3.3数据结构分析

.LPBX0结构即为gcov_info结构,定义如下。

[cpp]  view plain copy
  1. /* Type of function used to merge counters. */  
  2. typedef void (*gcov_merge_fn) (gcov_type *, gcov_unsigned_t);  
  3.    
  4. /* Information about counters. */  
  5. struct gcov_ctr_info  
  6. {  
  7.     gcov_unsigned_t num;    /* number of counters. */  
  8.     gcov_type *values;      /* their values. */  
  9.     gcov_merge_fn merge;    /* The function used to merge them. */  
  10. };  
  11.   
  12. /* Information about a single object file. */  
  13. struct gcov_info  
  14. {  
  15.     gcov_unsigned_t version; /* expected version number */  
  16.     struct gcov_info *next;  /* link to next, used by libgcov */  
  17.    
  18.     gcov_unsigned_t stamp;   /* uniquifying time stamp */  
  19.     const char *filename;    /* output file name */  
  20.    
  21.     unsigned n_functions;                 /* number of functions */  
  22.     const struct gcov_fn_info *functions; /* table of functions */  
  23.    
  24.     unsigned ctr_mask;                    /* mask of counters instrumented. */  
  25.     struct gcov_ctr_info counts[0];       /* count data. The number of bits 
  26.                                             set in the ctr_mask field 
  27.                                             determines how big this array is. */  
  28. };  

对应于上述代码中的解释,便一目了然。此处再重复一下对该结构的解释。

[plain]  view plain copy
  1. .align   32  
  2. .type    .LPBX0, @object  #.LPBX0是一个对象  
  3. .size    .LPBX0, 52       #.LPBX0大小为52字节  
  4. X0:                       #结构的起始地址,即结构名,该结构即为gcov_info结构  
  5. .long    875573616        #即version=0x34303170,即版本为4.1p  
  6. .long    0                #即next指针,为0,next为空  
  7. .long    -979544300       #即stamp=0xc59d5714  
  8. .long    .LC2             #filename,值为.LC2的常量  
  9. .long    1                #n_functions=1,1个函数  
  10. .long    .LC3             #functions指针,指向.LC3  
  11. .long    1                #ctr_mask=1  
  12. .long    5                #以下3个字段构成gcov_ctr_info结构,该字段num=5,即counter的个数  
  13. .long    .LPBX1           #values指针,指向.LPBX1,即5个counter的内容在.LPBX1结构中  
  14. .long    __gcov_merge_add #merge指针,指向__gcov_merge_add函数  
  15. .zero    12               #应该是12个0  

上述的.LC2即为文件名,如下。

[plain]  view plain copy
  1.     .section .rodata          #只读section  
  2.     .align   4  
  3. .LC2:                         #文件名常量,只读  
  4.     .string  "/home/zubo/gcc/test/test.gcda"  

然后就是functions结构,1个函数,函数结构就是.LC3的内容。

[plain]  view plain copy
  1. .LC3:  
  2.     .long    3            #ident=3  
  3.     .long    -345659544   #即checksum=0xeb65a768  
  4.     .long    5            #counters  

其对应的结构为gcov_fn_info,定义如下。

[cpp]  view plain copy
  1. / * Information about a single function. This uses the trailing array idiom. The number of  
  2.     counters is determined from the counter_mask in gcov_info. We hold an array of function    
  3.     info, so have to explicitly calculate the correct array stride. */  
  4. struct gcov_fn_info  
  5. {  
  6.     gcov_unsigned_t ident;    /* unique ident of function */   
  7.     gcov_unsigned_t checksum; /* function checksum */   
  8.     unsigned n_ctrs[0];       /* instrumented counters */  
  9. };  

3.4构造函数桩代码小结

 

gcov_init函数中的gcov_list是一个全局指针,指向gcov_info结构的链表,定义如下。

[cpp]  view plain copy
  1. / * Chain of per- object gcov structures. */  
  2. 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

你可能感兴趣的:(GCC Coverage代码分析-GCC插桩前后汇编代码对比分析)