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

晨曦之光 发布于 2012/03/09 14:17
阅读 314
收藏 0

【Gopher China万字分享】华为云的Go语言云原生实战经验!>>>

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

/**

 * filename: test.c

 */

#include

 

int main (void)

{

   int i, total;

 

   total = ;

 

   for (i= ;i < 10;i++)

       total += i;

 

   if (total!= 45)

       printf ("Failure/n");

   else

       printf ("Success/n");

   return ;

}

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汇编代码如下。#后面的注释为笔者所加。

   .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)   #比较循环变量i9的大小

   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        #返回值放入%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)   #比较循环变量i9的大小

   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        #返回值放入%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               #next指针,为

   .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,即5counter的内容在.LPBX1结构中

   .long    __gcov_merge_add #merge指针,指向__gcov_merge_add函数

   .zero    12          #应该是12

 

   .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

.LPBX1section属性可以看出该数组应该是rodata,即只读,其中的40应该就是其长度,即40字节。如下便是LPBX1数组,大小共40字节,以4字节方式对齐。

 

+0        +4        +8       +12     +16     +20     +24    +28      +32     +36

10

1

1

 

代码运行后,该数组的值就记录了桩代码被执行的次数,也即其后的代码块被执行的次数,如上所示。

 

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,))

    {

       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 : ;

               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 = ;

}

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

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

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

 

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

 

3.3数据结构分析

 

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

/* Type of function used to merge counters. */

typedefvoid (*gcov_merge_fn) (gcov_type *,gcov_unsigned_t);

 

/* Information about counters. */

structgcov_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. */

structgcov_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_infocounts[];      /* 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                   #next指针,为next为空

   .long    -979544300      #stamp=0xc59d5714

   .long    .LC2            #filename,值为.LC2的常量

   .long    1               #n_functions=11个函数

   .long    .LC3            #functions指针,指向.LC3

   .long    1               #ctr_mask=1

   .long    5               #以下3个字段构成gcov_ctr_info结构,该字段num=5,即counter的个数

   .long    .LPBX1          #values指针,指向.LPBX1,即5counter的内容在.LPBX1结构中

   .long    __gcov_merge_add #merge指针,指向__gcov_merge_add函数

   .zero    12              #应该是12

上述的.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. */

structgcov_fn_info

{

   gcov_unsigned_t ident;   /* unique ident of function */

   gcov_unsigned_t checksum;/* function checksum */

   unsigned n_ctrs[];      /* instrumented counters */

};

3.4构造函数桩代码小结

 

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

/ * Chain of per- object gcov structures. */

staticstruct gcov_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 标签:


原文链接:http://blog.csdn.net/livelylittlefish/article/details/6448906
加载中
返回顶部
顶部