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代码如下。

/**
 * filename: 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就是插入的部分桩代码。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)    #比较循环变量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

.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段桩代码,先不管他的功能,先分析一下以下代码。

    .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);
 
        /* 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 : 0;
                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 = 0;
}

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

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

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

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

3.3数据结构分析

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

/* Type of function used to merge counters. */
typedef void (*gcov_merge_fn) (gcov_type *, gcov_unsigned_t);
 
/* Information about counters. */
struct gcov_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. */
struct gcov_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_info counts[0];       /* 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    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;    /* unique ident of function */ 
    gcov_unsigned_t checksum; /* function checksum */ 
    unsigned n_ctrs[0];       /* instrumented counters */
};

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



Technorati 标签: 覆盖率测试,GCC,gcov,gcov-dump

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