嵌入式linux应用程序调试方法

嵌入式linux应用程序调试方法

四 内存工具    17
4.1 MEMWATCH    17
4.2 YAMD    22
4.3 Electric Fence    24
五 C/C++代码覆盖、性能profiling工具    24
5.1 用gcov来测试代码覆盖率    25
5.2 使用gprof来优化你的C/C++程序    35



四 内存工具
您肯定不想陷入类似在几千次调用之后发生分配溢出这样的情形。
许多小组花了许许多多时间来跟踪稀奇古怪的内存错误问题。应用程序在有的开发工作站上能运行,但在新的产品工作站上,这个应用程序在调用 malloc() 两百万次之后就不能运行了。真正的问题是在大约一百万次调用之后发生了溢出。新系统之所有存在这个问题,是因为被保留的 malloc() 区域的布局有所不同,从而这些零散内存被放置在了不同的地方,在发生溢出时破坏了一些不同的内容。 
我们用多种不同技术来解决这个问题,其中一种是使用调试器,另一种是在源代码中添加跟踪功能。在我职业生涯的大概也是这个时候,我便开始关注内存调试工具,希望能更快更有效地解决这些类型的问题。在开始一个新项目时,我最先做的事情之一就是运行 MEMWATCH 和 YAMD,看看它们是不是会指出内存管理方面的问题。
内存泄漏是应用程序中常见的问题,不过您可以使用本文所讲述的工具来解决这些问题。

内存调试工具

C 语言作为 Linux 系统上标准的编程语言给予了我们对动态内存分配很大的控制权。然而,这种自由可能会导致严重的内存管理问题,而这些问题可能导致程序崩溃或随时间的推移导致性能降级。
内存泄漏(即 malloc() 内存在对应的 free() 调用执行后永不被释放)和缓冲区溢出(例如对以前分配到某数组的内存进行写操作)是一些常见的问题,它们可能很难检测到。这一部分将讨论几个调试工具,它们极大地简化了检测和找出内存问题的过程。

4.1 MEMWATCH
MEMWATCH 由 Johan Lindh 编写,是一个开放源代码 C 语言内存错误检测工具,您可以自己下载它(请参阅本文后面部分的参考资料)。只要在代码中添加一个头文件并在 gcc 语句中定义了 MEMWATCH 之后,您就可以跟踪程序中的内存泄漏和错误了。MEMWATCH 支持 ANSI C,它提供结果日志纪录,能检测双重释放(double-free)、错误释放(erroneous free)、没有释放的内存(unfreed memory)、溢出和下溢等等。

    快速上手:
在MEMWATCH的源代码包中包含有三个程序文件:memwatch.c, memwatch.h, test.c; 可以利用这三个程序,按照你的编译环境对makefile修改后,就可是简单使用memwatch来检测test.c这个文件中的内存问题;


当然你可以根据自己的需要修改test.c文件,不过需要注意的是:
1)如果你的程序是永远不会主动退出的话,建议深入看看memwatch,因为memwatch默认情况下,在程序退出的时,将有关内存检测的结果写入到某个日志文件中;所以针对你永远不会退出的程序,你需要自己动手调用MEMWATCH的结果函数。
2)如果你要使用memwatch来检测内存问题的话,必须将memwatch.h头文件包含在你需要检测文件中;并且把memwatch.c加入编译。在编译时记得加-DMEMWATCH -DMW_STDIO选项;在主文件中最好包含以下信息,
//在linux下需要包含signal.h这个头文件
#ifndef SIGSEGV
#error "SIGNAL.H does not define SIGSEGV; running this program WILL cause a core dump/crash!"
#endif

//防止你在编译时没有加 -DMEMWATCH
#ifndef MEMWATCH
#error "You really, really don't want to run this without memwatch. Trust me."
#endif

//防止你在编译时候没有加 -DMEMWATCH_STDIO,在一些指针错误的时候,会弹出
//Abort/Retry/Ignore 让你选择处理方法。
#if !defined(MW_STDIO) && !defined(MEMWATCH_STDIO)
#error "Define MW_STDIO and try again, please."
#endif

//多线程程序需要定义MW_PTHREADS,虽然它并不一定能够保证多线程安全,zpf
#if !defined(MW_PTHREADS) && !defined(HAVE_PTHREAD_H)
#error "if ur program is multithreads, please define MW_PTHREADS; "
#error "otherwise, Comment out the following line."
#endif

3)刚开始还是找一个简单的单线程程序来上手吧.


    样例讲解

清单 1. 内存样本(test1.c) 

#include <stdlib.h>
#include <stdio.h>
#include "memwatch.h"

int main(void)
{
  char *ptr1;
  char *ptr2;

  ptr1 = malloc(512);
  ptr2 = malloc(512);

  ptr2 = ptr1;
  free(ptr2);
  free(ptr1);
}
清单 1 中的代码将分配两个 512 字节的内存块,然后指向第一个内存块的指针被设定为指向第二个内存块。结果,第二个内存块的地址丢失,从而产生了内存泄漏。
现在我们编译清单 1 的 memwatch.c。下面是一个 makefile 示例:
test1 

gcc -DMEMWATCH -DMW_STDIO test1.c memwatch
c -o test1
当您运行 test1 程序后,它会生成一个关于泄漏的内存的报告。清单 2 展示了示例 memwatch.log 输出文件。
清单 2. test1 memwatch.log 文件 

MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh

...
double-free: <4> test1.c(15), 0x80517b4 was freed from test1.c(14)
...
unfreed: <2> test1.c(11), 512 bytes at 0x80519e4
{FE FE FE FE FE FE FE FE FE FE FE FE ..............}

Memory usage statistics (global):
  N)umber of allocations made:     2
  L)argest memory usage :     1024
  T)otal of all alloc() calls:     1024
  U)nfreed bytes totals :     512
MEMWATCH 为您显示真正导致问题的行。如果您释放一个已经释放过的指针,它会告诉您。对于没有释放的内存也一样。日志结尾部分显示统计信息,包括泄漏了多少内存,使用了多少内存,以及总共分配了多少内存。

建议:
建议在你需要测试的主文件中加入以下函数信息

//在linux下需要包含signal.h这个头文件
#ifndef SIGSEGV
#error "SIGNAL.H does not define SIGSEGV; running this program WILL cause a core dump/crash!"
#endif

//防止你在编译时没有加 -DMEMWATCH
#ifndef MEMWATCH
#error "You really, really don't want to run this without memwatch. Trust me."
#endif

//防止你在编译时候没有加 -DMEMWATCH_STDIO,在一些指针错误的时候,会弹出
//Abort/Retry/Ignore 让你选择处理方法。
#if !defined(MW_STDIO) && !defined(MEMWATCH_STDIO)
#error "Define MW_STDIO and try again, please."
#endif

//多线程程序需要定义MW_PTHREADS,虽然它并不一定能够保证多线程安全,zpf
#if !defined(MW_PTHREADS) && !defined(HAVE_PTHREAD_H)
#error "if ur program is multithreads, please define MW_PTHREADS; "
#error "otherwise, Comment out the following line."
#endif


另外需要说明的是,在源代码附带的<<USING>>文档中,作者说不能保证代码是绝对多线程安全的,同时如果你如果要使用它来检测linux下多线程程序的内存情况时,请定义MW_PTHREADS宏(可以通过在makefile中加 –DMW_PTHREADS),这样memwatch将引入互斥量操作。


MEMWATCH程序使用中通过简单的加入memwatch.h头文件,并且将memwatch.c加入编译,就可以在程序正常结束的时候,输出日志文件,在日志文件中给出系统中的内存问题;但是有一个问题,在我们的终端中应用程序是永远不会退出的,并且可能系统根本就没有充分考虑良好的收尾工作;那么如果使用MEMWATCH来检测我们的程序是否存在内存问题哪?
    仔细阅读memwatch.h文件会发现,MEMWATCH提供了两个比较有用的函数:
void  mwInit( void );
void  mwTerm( void );
关于这两个函数,作者给出的注释为:Normally, it is not nessecary to call any of these. MEMWATCH will automatically initialize itself on the first MEMWATCH function call, and set up a call to mwAbort() using atexit()。所以通常我们不需要显式调用这两个函数。 
但是问题是,我们的程序可能不会显式的退出,从而atexit()函数不能正常被调用;在这种情况下,我们可以通过显式的调用来完成一定的内存检测工作;
mwInit( void ) 和mwTerm( void )可以多次调用。但是两个和malloc和free一样,必须配对使用。你就可以通过这样方式控制MEMWATCH什么时候开始检测内存,什么时候结束检测内存;

    MEMWATCH日志输出
MEMWATCH会输出日志到程序工作目录的memwatch.log文件中,如果这个文件不能创建的话,程序会尝试创建memwatNN.log,NN 从01 到 99。
如果你觉得没有必要输出日志到文件中,那么可以自己定义输出函数,方法为输出函数必须为void func(int c)类型,然后将输出函数地址作为参数显式调用mwSetOutFunc()指定输出;这样程序就会重定向输出到你定义的输出函数中;
    野指针
当使用没有初始化的指针,或者指针指向的空间已被移动或者释放时,就会造成野指针问题。
避免这个问题的最好方法是:定义指针时总是初始化为NULL;当释放指针后,显式的将指针设置为NULL;
为了便于追踪这种指针错误,MEMWATCH程序将所有的内存区域赋予特定的数字;例如:刚分配但是未初始化的内存区域全部设置为0xFE;最近释放后没有被重新利用的内存全部设置为0xFD;所以如果你的程序在没有使用MEMWATCH时没有问题,但是加载MEMWATCH后会崩溃的话,可能程序中就存在指针分配后,没有初始化就使用;或者使用了已经释放的内存。

    更深入的使用
更深入的使用请参见,<<USING>>, <<README>>, <<memwatch.h>>. 



我目前做的实验:在一个进程中(是否多个进程能够同时使用,还由待验证),初始化阶段,显式调用mwInit(), 然后在处理SIGINT,SIGTERM(因为程序运行过程中,我们会利用killall杀死我们不会停止的程序)的函数中显式调用mwTerm()函数; 另外我还修改了static void mwWrite( const char *format, ... )这个函数,在将有关信息输出到日志的过程中,同时利用printf输出到屏幕上(当然你也可以通过调用mwSetOutFunc()指定自己的输出函数)。如果是多线程的话,请定义MW_PTHREADS宏(可以通过在makefile中加 –DMW_PTHREADS)。

    单线程:
RT-VD4201M和RT-VS4201S的主线程都不存在问题;

    多线程:
在RT-VD4201M的多线程程序中没有问题, 可以检测出那些没有被释放的内存; 在RT-VS4201S的主线程单独使用时,没有问题,可以检测出内存泄漏;而在RT-VS4201S多个线程中,常会出现在创建线程时程序崩溃现象,有时候程序可能会运行下去;具体原因不明,可能是作者提到的“并不能完全确保函数多线程安全”,或者“程序存在前面提到的野指针问题”,或者其它问题。
使用MEMWATCH初步对RT-VD4201M和RT-VS4201S主程序进行内存检测;除了一些程序初始化阶段分配的动态内存外(这些内存泄漏是程序已知的,例如RTSP,MP模块初始化分配的内存),近一个小时的运行,并没有发现其他内存泄漏问题。

    多进程:
多个进程也可以支持。



4.2 YAMD
(说明:资料从网上来,该软件没有试用过)
YAMD 软件包由 Nate Eldredge 编写,可以查找 C 和 C++ 中动态的、与内存分配有关的问题。在撰写本文时,YAMD 的最新版本为 0.32。请下载 yamd-0.32.tar.gz(请参阅参考资料)。执行 make 命令来构建程序;然后执行 make install 命令安装程序并设置工具。
一旦您下载了 YAMD 之后,请在 test1.c 上使用它。请删除 #include memwatch.h 并对 makefile 进行如下小小的修改:
使用 YAMD 的 test1 

gcc -g test1.c -o test1
清单 3 展示了来自 test1 上的 YAMD 的输出。
清单 3. 使用 YAMD 的 test1 输出 

YAMD version 0.32
Executable: /usr/src/test/yamd-0.32/test1
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal deallocation of this block
Address 0x40025e00, size 512
...
ERROR: Multiple freeing At
free of pointer already freed
Address 0x40025e00, size 512
...
WARNING: Memory leak
Address 0x40028e00, size 512
WARNING: Total memory leaks:
1 unfreed allocations totaling 512 bytes

*** Finished at Tue ... 10:07:15 2002
Allocated a grand total of 1024 bytes 2 allocations
Average of 512 bytes per allocation
Max bytes allocated at one time: 1024
24 K alloced internally / 12 K mapped now / 8 K max
Virtual program size is 1416 K
End.
YAMD 显示我们已经释放了内存,而且存在内存泄漏。让我们在清单 4 中另一个样本程序上试试 YAMD。
清单 4. 内存代码(test2.c) 

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
  char *ptr1;
  char *ptr2;
  char *chptr;
  int i = 1;
  ptr1 = malloc(512);
  ptr2 = malloc(512);
  chptr = (char *)malloc(512);
  for (i; i <= 512; i++) {
    chptr[i] = 'S';
  }    
  ptr2 = ptr1;
  free(ptr2);
  free(ptr1);
  free(chptr);
}
您可以使用下面的命令来启动 YAMD:
./run-yamd /usr/src/test/test2/test2 
清单 5 显示了在样本程序 test2 上使用 YAMD 得到的输出。YAMD 告诉我们在 for 循环中有“越界(out-of-bounds)”的情况。
清单 5. 使用 YAMD 的 test2 输出 

Running /usr/src/test/test2/test2
Temp output to /tmp/yamd-out.1243
*********
./run-yamd: line 101: 1248 Segmentation fault (core dumped)
YAMD version 0.32
Starting run: /usr/src/test/test2/test2
Executable: /usr/src/test/test2/test2
Virtual program size is 1380 K
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal allocation of this block
Address 0x4002be00, size 512
ERROR: Crash
...
Tried to write address 0x4002c000
Seems to be part of this block:
Address 0x4002be00, size 512
...
Address in question is at offset 512 (out of bounds)
Will dump core after checking heap.
Done.
MEMWATCH 和 YAMD 都是很有用的调试工具,它们的使用方法有所不同。对于 MEMWATCH,您需要添加包含文件 memwatch.h 并打开两个编译时间标记。对于链接(link)语句,YAMD 只需要 -g 选项。


4.3 Electric Fence
(说明:资料从网上来,该软件没有试用过)
多数 Linux 分发版包含一个 Electric Fence 包,不过您也可以选择下载它。Electric Fence 是一个由 Bruce Perens 编写的 malloc() 调试库。它就在您分配内存后分配受保护的内存。如果存在 fencepost 错误(超过数组末尾运行),程序就会产生保护错误,并立即结束。通过结合 Electric Fence 和 gdb,您可以精确地跟踪到哪一行试图访问受保护内存。Electric Fence 的另一个功能就是能够检测内存泄漏。


五 C/C++代码覆盖、性能profiling工具

C/C++代码覆盖、性能profiling工具一般基于GNU的gprof和gcov。还有一类基于模拟器的profiling工具,如IBM Purify, Valgrind。KCahcegrind是Callgrind,OProfile等的GUI前端。性能测试工具有ggcof,kprof,lcov等等。lcov是Linux Testing Project工具之一,见http://ltp.sourceforge.net/tooltable.php上的工具列表。这儿还有压力测试、WEB Server测试等许多工具。在http://www.testingfaqs.org分类归纳了多种软件测试工具。

5.1 用gcov来测试代码覆盖率 

gcov是gnu/gcc工具库中的一个组件, 用来测试代码的覆盖率;当构建一个程序时,gcov会监视一个程序的执行,并且会标识出执行了哪一行源码,哪一行没有执行。更进一步,gcov可以标识出某一行源执行的次数,这样就可以知道程序在哪里花费了大多数的时间。

为什么要测试代码覆盖率?
我是不喜欢在代码中有跑不到的地方,那只是在白白浪费空间,降低效率
当然了,有些时候,我们可以通过跑代码覆盖率来发现我们有什么异常情况没有进行测试,毕竟单元测试的用例,不可能一下就想的很全面的。
例如,你的程序在某个函数的入口前处检测了指针不为空,你进入调用函数以后又检测了一回这个指针,并且对为NULL的情况进行处理,那么两处之中必有一处是在浪费空间,当然你的硬盘大,放的下,但是代码写的精致一些,不是更好么?

    获得gcov
gcov是gnu/gcc工具库的组件,所以在建立交叉编译工具的时候需要指定创建这个工具。
在arm-linux-的交叉编译工具中,arm-linux-gcov好像默认是存在的;操作系统组给我的交叉编译环境中是有这一工具的;但是uClinux的交叉编译环境中默认好像是没有这个工具的;具体的搭建从下面gprof的讨论也许能够得到一些信息。因为现在我还没有移植操作系统的经验,所以无法对这个进行证实。
关于这个论坛上面的讨论是:
> The gprof program, for historical reasons, is sometimes excluded
> from a cross-targeted toolchain.  If you have a source tree with
> a Cygnus configure script at the top level, or a gcc source tree,
> then look for the "native_only" variable from the top-level
> configure.in, remove "gprof".  Then reconfigure the build tree,
> and run "make" in the gprof subdirectory.

That did it!

Just to be clear, this is what I did:

I untar'd the binutils-2.12.1 tar ball and edited 
binutils-2.12.1/configure.in. There's a line containing "native_only" that 
several packages (like gprof, sed,...); I removed gprof from that list.

I then followed the instructions here for building binutils:
http://sources.redhat.com/ecos/tools/linux-arm-elf.html

and arm-elf-gprof was created! Score!




    使用gcov

使用gcov很简单, 首先在编译的时候加上-fprofile-arcs -ftest-coverage,同时链接的时候也加上这些选项;需要特别说明的时,gcov要求被测试程序在执行的时能够访问到它编译的那个目录,因为要使用到编译过程中生成的一个文件(我测试的时是这样的,执行时提示找不到/home/zpf/gdb_test/bubblesort.gcda这个文件,而我得bubblesort程序是在/home/zpf/gdb_tes/这个目录里编译的);所以如果是嵌入式的话,就需要nfs这样的工具支持。
    示例程序源代码
例如我们需要测试以下bubblesort的代码:

1:       #include <stdio.h>
2:
3:       void bubbleSort( int list[], int size )
4:       {
5:         int i, j, temp, swap = 1;
6:
7:         while (swap) {
8:
9:           swap = 0;
10:
11:           for ( i = (size-1) ; i >= 0 ; i— ) {
12:
13:             for ( j = 1 ; j <= i ; j++ ) {
14:
15:               if ( list[j-1] > list[j] ) {
16:
17:                 temp = list[j-1];
18:                 list[j-1] = list[j];
19:                 list[j] = temp;
20:                 swap = 1;
21:
22:               }
23:
24:             }
25:
26:           }
27:
28:         }
29:
30:       }
31:
32:       int main()
33:       {
34:         int theList[10]={10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
35:         int i;
36:
37:         /* Invoke the bubble sort algorithm */
38:         bubbleSort( theList, 10 );
39:
40:         /* Print out the final list */
41:         for (i = 0 ; i < 10 ; i++) { 
42:           printf("%d
", theList[i]);
43:         }
44:
45:       }


    编译程序
如果要使用gcov进行覆盖测试,在编译程序时,必须加-fprofile-arcs -ftest-coverage编译选项;下面是我们用来演示编译bubbleSort.c的命令:
    gcc bubblesort.c -o bubblesort -ftest-coverage -fprofile-arcs

当我们执行生成的bubblesort程序时会生成一些包含关于程序的相关数据的文件。gcov程序将会使用这些文件来报告数据并且向开发者提供相应的信息。当指定“-ftest-coverage”(注意这是一个选项而不是两个选项)选项时会为每一个源码生成两个文件,他们以“.bb”与“.bbg”为扩展名,并且用这些文件来重组每一个可执行程序的程序流图。当指定“-fprofile-arcs” (注意这是一个选项而不是两个选项),将会生成一个包含每一个指令分支的执行计数的以“.da”为扩展名的文件。这些文件会在执行以后与源码文件一起使用,来标识源码的执行行为。
    运行程序
运行刚才编译生成的bubblesort程序就会生成我们在前面所讨论的那些附带文件。然后我们使用我们希望进行检测的源码运行gcov程序。如下面所示:
$ ./bubblesort
...
$ gcov bubblesort.c
100.00% of 17 source lines executed in file bubblesort.c
Creating bubblesort.c.gcov.

以上信息告诉我们,在这个例子程序中所有的源码行至少都执行了一次。另外还可以通过查看生成的bubblesort.c.gcov文件来了解每一源码行所实际运行的次数。如下面所示:

-:    0:Source:bubblesort.c
        -:    0:Graph:bubblesort.gcno
        -:    0:Data:bubblesort.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:#include <stdio.h>
        -:    2:void bubbleSort(int list[],int size)
        1:    3:{
        1:    4:    int i,j,temp,swap=1;
        4:    5:    while(swap)
        -:    6:    {
        2:    7:    swap=0;
       22:    8:    for(i=(size-1);i>=0;i--)
        -:    9:    {
      110:   10:        for(j=1;j<=i;j++)
        -:   11:        {
       90:   12:        if(list[j-1]>list[j])
        -:   13:        {
       45:   14:            temp=list[j-1];
       45:   15:            list[j-1]=list[j];
       45:   16:            list[j]=temp;
       45:   17:            swap=1;
        -:   18:        }
        -:   19:        }
        -:   20:    }
        -:   21:    }
        1:   22:}
        -:   23:int main()
        1:   24:{
        1:   25:    int theList[10]={10,9,8,7,6,5,4,3,2,1};
        -:   26:    int i;
        -:   27:    /*Invoke the buble sort algorithm*/
        1:   28:    bubbleSort(theList,10);
        -:   29:
        -:   30:    /*print out the final list*/
       11:   31:    for(i=0;i<10;i++)
        -:   32:    {
       10:   33:    printf("%d
",theList[i]);
        -:   34:    }
        1:   35:    return 0;
        -:   36:}


第一列显示了源码中每一行源码所执行的次数。在一些情况下,执行次数并没有提供,因为他们是并不会影响代码的简单C源码元素。
这些计数可以提供一些关于程序执行的信息。例如,第12行执行了90次,而14-17行的代码只是执行了45次。这告诉我们当这个函数调用了90次,真正成功的仅是45次。换句话说,大部分的测试时间浪费在两个元素的交换上。这是由于测试数据的顺序所造成的。
从这里我们可以看到代码段中最常执行的部分就是排序算法的内循环部分。这是因为由于退出测试第10行要比第12行执行的次数多一些。
遗憾的是:我使用arm交叉编译环境按照上面的步骤编译程序,下载到目标板上运行时程序可以正常执行;但是没有日志文件产生;提示为“profiling:/home/zpf/gdb_test/bubblesort.gcda:Cannot open”;“/home/zpf/gdb_test/”是我在linux服务器上编译bubblesort的目录;从这里看,要想利用gcov进行覆盖测试的话,必须在你编译的那个目录执行,看来nfs是嵌入式调试的根本???



    查看分支效率

使用-b选项可以查看程序的分支效率。这个选项会输出程序中每一个分支的频度与相应的摘要。例如,我们使用-b选项来执行gcov命令:
$ gcov -b bubblesort.c
100.00% of 17 source lines executed in file bubblesort.c
100.00% of 12 branches executed in file bubblesort.c
100.00% of 12 branches taken at least once in file bubblesort.c
100.00% of 2 calls executed in file bubblesort.c
Creating bubblesort.c.gcov.


所生成的bubblesort.c.gcov文件如下所示。

-:    0:Source:bubblesort.c
        -:    0:Graph:bubblesort.gcno
        -:    0:Data:bubblesort.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:#include <stdio.h>
        -:    2:void bubbleSort(int list[],int size)
function bubbleSort called 1 returned 100% blocks executed 100%
        1:    3:{
        1:    4:    int i,j,temp,swap=1;
        4:    5:    while(swap)
branch  0 taken 67%
branch  1 taken 33% (fallthrough)
        -:    6:    {
        2:    7:    swap=0;
       22:    8:    for(i=(size-1);i>=0;i--)
branch  0 taken 91%
branch  1 taken 9% (fallthrough)
        -:    9:    {
      110:   10:        for(j=1;j<=i;j++)
branch  0 taken 82%
branch  1 taken 18% (fallthrough)
        -:   11:        {
       90:   12:        if(list[j-1]>list[j])
branch  0 taken 50% (fallthrough)
branch  1 taken 50%
        -:   13:        {
       45:   14:            temp=list[j-1];
       45:   15:            list[j-1]=list[j];
       45:   16:            list[j]=temp;
       45:   17:            swap=1;
        -:   18:        }
        -:   19:        }
        -:   20:    }
        -:   21:    }
        1:   22:}
        -:   23:int main()
function main called 1 returned 100% blocks executed 100%
        1:   24:{
        1:   25:    int theList[10]={10,9,8,7,6,5,4,3,2,1};
        -:   26:    int i;
        -:   27:    /*Invoke the buble sort algorithm*/
        1:   28:    bubbleSort(theList,10);
call    0 returned 100%
        -:   29:
        -:   30:    /*print out the final list*/
       11:   31:    for(i=0;i<10;i++)
branch 0 taken = 91%
branch 1 taken = 100%
branch 2 taken = 100%
        -:   32:    {
       10:   33:    printf("%d
",theList[i]);
call    0 returned 100%
        -:   34:    }
        1:   35:    return 0;
        -:   36:}


与前一个的文件类似,但是这一次每一个分支点都用他们的频度进行了标示。
分支点依赖于目标结构指令集(看来要好好理解分支的含义,还需要对汇编有一定的了解)。第12行是一个简单的if语句,所以有一个分支点。在这里我们可以注意到这是50%,这通过我们前面观察程序的执行次数可以看出。其他的分支点有一些难于分析。例如,
第7行是一个while语句,有两个分支点。在X86汇编中,这一行编译成我们下面所看到的样子:
1:   cmpl        $0, -20(%ebp)
2:   jne        .L4
3:   jmp        .L1
从这里我们可看出,swap变量与0进行比较。如果他不等于0,就会跳转到第2行,.L4。否则要跳转到第3行,.L1。第2行所示的分支概率为67%,这是因为这一行执行3次,但是jne只执行了两次。当第2行的jne并没有执行时,我们直接跳转到第3行。这只执行一次,但是一旦执行,程序就结束了。所以分支1要花费100%的时间。

第33行的printf()调用;其下面有一行call 0 returned 100%;  关于这个在手册上是如此解释的:如果一个函数调用至少被执行一次的话,gcov给出的百分比表示“函数返回次数 除以 函数调用次数”, 通常这个数值是100%,但是对于象“exit”或者“longjmp”这类并不是每次调用都返回的函数来说,这个百分比就小于100%。(For a call, if it was executed at least once, then a percentage indicating the number of times the call returned divided by the number of times the call was executed will be printed.  This will usually be 100%, but may be less for functions call "exit" or "longjmp", and thus may not return every time they are called.)

疑惑的地方:这里的分支是如何划分的,一个for怎么有三个分支,并且每个分支都执行到了??是不是“i=0;i<10;i++”认为是3个分支?
 关于分支百分比,手册上是这样解释的:如果某个分支从没被执行过,则给出“never executed”;否则gcov给出的百分比说明这个分支“taken的次数”除以“executed的次数”(郁闷这里take如何理解那???)(For a branch, if it was executed at least once, then a percentage indicating the number of times the branch was taken divided by the number of times the branch was executed will be printed.  Otherwise, the message ``never executed'' is printed.)。

所以分支概率在理解程序流时是相当有用的,但是要参考汇编代码,需要理解分支点在哪里。
看起来有点晕


    不完整程序测试

当gcov计数一个测试并不是100%的程序时,并没有执行的行是标记为####,而不是执行次数。下面显示的是一个由gcov创建的文件来显示少于100%的测试。


 1:                #include <stdio.h>
 2:
 3:                int main()
 4:           1    {
 5:           1      int a=1, b=2;
 6:
 7:           1      if (a == 1) {
 8:           1        printf("a = 1
");
 9:                  } else {
 10:      ######        printf("a != 1
");
 11:                  }
 12:
 13:           1      if (b == 1) {
 14:      ######        printf("b = 1
");
 15:                  } else {
 16:           1        printf("b != 1
");
 17:                  }
 18:
 19:           1      return 0;
 20:                }


 当这个程序运行时,gcov也会向标准输出输出相应的信息。他会显示可能执行的源码行的行数以及实际运行的百分比。


 $ gcov incomptest.c
 77.78% of 9 source lines executed in file incomptest.c
 Creating incomptest.c.gcov.
 $


如果我们的例子程序有多个函数,我们可以通过使用-f选项来查看每一个函数的执行情况。如下面的我们以bubbleSort程序所进行的演示:

$ gcov -f  bubblesort.c
100.00% of 11 source lines executed in function bubbleSort
100.00% of 6 source lines executed in function main
100.00% of 17 source lines executed in file bubblesort.c
Creating bubblesort.c.gcov.
$


    gcov可用的选项

gcov程序调用的格式为:
gcov [options] sourcefile 
其可用的选项如下:
选项                        目的
-v,-version                打印版本信息
-h,-help                    打印帮助信息
-b,-branch-probabilities    向输出文件输出分支频度
-c,-branch-counts          打印分支计数而不是分支频度
-n,-no-output               不创建gcov输出文件
-l,-long-file-names         创建长文件名
-f,-function-summaries     打印每一个函数的概要
-o,-object-directory        .bb,.bbg,.da文件存放的目录

从上面这个表中,我们可以看到一个单个字符选项,以及一个长选项。当从命令行中使用gcov命令时短选项是比较有用的,但是当gcov是Makefile的一个部分时,应使用长选项,因为这更易于理解。
当了解gcov程序的版本信息时,可以使用-v选项。因为gcov是与一个指定的编译器工具链联系在一起的(实际上是由gcc工具链而构建的),gcc版本与gcov的版本是相同的。gcov程序的简介以及选项帮助可以用-h选项来进行显示。

在你的目录下多了几个文件,后缀是.gcda,gcno,呵呵,恭喜你,你的gcov的版本还是比较新的,在以前旧一点的版本中,有.bb,.bbg这种后缀(我pc上安装的是2.4.18-14的内核,生成了以这个为扩展名的文件),不过现在都没了,其实gcov跑的数据统计什么的都保存在这些文件中,这就是为什么,你可以多次跑程序,而gcov会自己统计的神奇本领所在。     

另外在man gcov中还提到,gprof可以使用gcov生成的日志文件进一步分析性能,原文如下:gcov creates a logfile called sourcefile.gcov which indicates how many times each line of a source file sourcefile.c has executed.  You can use these logfiles along with gprof to aid in fine-tuning the performance of your programs.  gprof gives timing information you can use along with the information you get from gcov.

gcov [-b] [-c] [-v] [-n] [-l] [-f] [-o directory] sourcefile

-b
    Write branch frequencies to the output file, and write branch summary info to the standard output. This option allows you to see how often each branch in your program was taken.
    //b(ranch),分支测试
-c
    Write branch frequencies as the number of branches taken, rather than the percentage of branches taken. 

-v
    Display the gcov version number (on the standard error stream).

-n
    Do not create the gcov output file.

-l
    Create long file names for included source files. For example, if the header file `x.h' contains code, and was included in the file `a.c', then running gcov on the file `a.c' will produce an output file called `a.c.x.h.gcov' instead of `x.h.gcov'. This can be useful if `x.h' is included in multiple source files.

-f
    Output summaries for each function in addition to the file level summary.

-o
    The directory where the object files live. Gcov will search for `.bb', `.bbg', and `.da' files in this directory. 
    //新版的是这么说的
     -o directory│file
       --object-directory directory
       --object-file file
           Specify either the directory containing the gcov data files, or the
           object path name. The .gcno, and .gcda data files are searched for
           using this option. If a directory is specified, the data files are
           in that directory and named after the source file name, without its
           extension. If a file is specified here, the data files are named
           after that file, without its extension. If this option is not sup-
           plied, it defaults to the current directory.


其他的还有新版的-u,
     -u
       --unconditional-branches
           When branch counts are given, include those of unconditional
           branches.  Unconditional branches are normally not interesting.
      -p
       --preserve-paths
           Preserve complete path information in the names of generated .gcov
           files. Without this option, just the filename component is used.
           With this option, all directories are used, with ’/’ characters
           translated to ’#’ characters, ’.’ directory components removed and
           ’..’  components renamed to ’^’. This is useful if sourcefiles are
           in several different directories. It also affects the -l option.

    man一下就能看到,我也不多说了

    Using gcov with GCC Optimization

 If you plan to use gcov to help optimize your code, you must first com-
 pile your program with two special GCC options: -fprofile-arcs
 -ftest-coverage.  Aside from that, you can use any other GCC options;
 but if you want to prove that every single line in your program was
 executed, you should not compile with optimization at the same time.
 On some machines the optimizer can eliminate some simple code lines by
 combining them with other lines.  For example, code like this:

         if (a != b)
           c = 1;
         else
           c = 0;

 can be compiled into one instruction on some machines.  In this case,
 there is no way for gcov to calculate separate execution counts for
 each line because there isn't separate code for each line.  Hence the
 gcov output looks like this if you compiled the program with optimiza-
 tion:

               100  if (a != b)
               100    c = 1;
               100  else
               100    c = 0;

 The output shows that this block of code, combined by optimization,
 executed 100 times.  In one sense this result is correct, because there
 was only one instruction representing all four of these lines.  How-
 ever, the output does not indicate how many times the result was 0 and
 how many times the result was 1.


5.2 使用gprof来优化你的C/C++程序

作者:arnouten(Q)bzzt.net linuxfocus (2005-05-18 15:03:24) 中文编译: 小 汪   

谨记:在值得优化的地方优化!没有必要花几个小时来优化一段实际上只运行0.04秒的程序。 

gprof是GNU profiler工具。可以显示程序运行的“flat profile”,包括每个函数的调用次数,每个函数消耗的处理器时间。也可以显示“调用图”,包括函数的调用关系,每个函数调用花费了多少时间。还可以显示“注释的源代码”,是程序源代码的一个复本,标记有程序中每行代码的执行次数。
gprof 使用了一种异常简单但是非常有效的方法来优化C/C++ 程序,而且能很容易的识别出值得优化的代码。一个简单的案例分析将会显示,GProf如何通过识别并优化两个关键的数据结构,将实际应用中的程序从3分钟的运行时优化到5秒的。

    获得gprof

在gprof这个工具之前,当前首先要获得这个工具,gporf是gnu/gcc工具集中的一个工具,但是按照论坛上的说法,通常在生成工具集时,这个工具默认是不产生的,需要修改配置文件才能获得。
关于这个论坛上面的讨论是:

> The gprof program, for historical reasons, is sometimes excluded
> from a cross-targeted toolchain.  If you have a source tree with
> a Cygnus configure script at the top level, or a gcc source tree,
> then look for the "native_only" variable from the top-level
> configure.in, remove "gprof".  Then reconfigure the build tree,
> and run "make" in the gprof subdirectory.

That did it!

Just to be clear, this is what I did:

I untar'd the binutils-2.12.1 tar ball and edited 
binutils-2.12.1/configure.in. There's a line containing "native_only" that 
several packages (like gprof, sed,...); I removed gprof from that list.

I then followed the instructions here for building binutils:
http://sources.redhat.com/ecos/tools/linux-arm-elf.html

and arm-elf-gprof was created! Score!





    使用gprof

程序概要分析的概念非常简单:通过记录各个函数的调用和结束时间,我们可以计算出程序的最大运行时的程序段。这种方法听起来似乎要花费很多气力——幸运的是,我们其实离真理并不远!我们只需要在用 gcc 编译时加上一个额外的参数('-pg'),运行这个(编译好的)程序(来搜集程序概要分析的有关数据),然后运行“gprof”以更方便的分析这些结果。   

案例分析: Pathalizer
我使用了一个现实中使用的程序来作为例子,是 pathalizer的一部分: 即event2dot,一个将路径“事件”描述文件转化为图形化“dot”文件的工具(executable which translates a pathalizer 'events' file to a graphviz 'dot' file)。 

简单的说,它从一个文件里面读取各种事件,然后将它们分别保存为图像(以页为节点,且将页与页之间的转变作为边),然后将这些图像整合为一张大的图形,并保存为图形化的'dot'格式文件。   

给程序计时
先让我们给我们未经优化的程序计一下时,看看它们的运行要多少时间。在我的计算机上使用event2dot并用源码里的例子作为输入(大概55000的数据),大致要三分多钟: 


real    3m36.316s
user    0m55.590s
sys     0m1.070s

  

程序分析
要使用gprof 作概要分析,在编译的时候要加上'-pg' 选项,我们就是如下重新编译源码如下: 

g++ -pg dotgen.cpp readfile.cpp main.cpp graph.cpp config.cpp -o event2dot

编译时编译器会自动在目标代码中插入用于性能测试的代码片断,这些代码在程序在运行时采集并记录函数的调用关系和调用次数,以及采集并记录函数自身执行时间和子函数的调用时间,程序运行结束后,会在程序退出的路径下生成一个gmon.out文件。这个文件就是记录并保存下来的监控数据。可以通过命令行方式的gprof或图形化的Kprof来解读这些数据并对程序的性能进行分析。另外,如果想查看库函数的profiling,需要在编译是再加入“-lc_p”编译参数代替“-lc”编译参数,这样程序会链接libc_p.a库,才可以产生库函数的profiling信息。如果想执行一行一行的profiling,还需要加入“-g”编译参数。

现在我们可以再次运行event2dot,并使用我们前面使用的测试数据。这次我们运行的时候,event2dot运行的分析数据会被搜集并保存在'gmon.out'文件中,我们可以通过运行'gprof event2dot | less'来查看结果。 

gprof 会显示出如下的函数比较重要: 

 % cumulative  self              self     total
 time seconds  seconds  calls s/call s/call name
43.32   46.03  46.03 339952989  0.00  0.00 CompareNodes(Node *,Node *)
25.06   72.66  26.63    55000   0.00  0.00 getNode(char *,NodeListNode *&)
16.80   90.51  17.85 339433374  0.00  0.00 CompareEdges(Edge *,AnnotatedEdge *)
12.70  104.01  13.50    51987   0.00  0.00 addAnnotatedEdge(AnnotatedGraph *,Edge *)
 1.98  106.11   2.10    51987   0.00  0.00 addEdge(Graph *,Node *,Node *)
 0.07  106.18   0.07        1   0.07  0.07 FindTreshold(AnnotatedEdge *,int)
 0.06  106.24   0.06        1   0.06 28.79 getGraphFromFile(char *,NodeListNode *&,Config *)
 0.02  106.26   0.02        1   0.02 77.40 summarize(GraphListNode *,Config *)
 0.00  106.26   0.00    55000   0.00  0.00 FixName(char *)

可以看出,第一个函数比较重要: 程序里面绝大部分的运行时都被它给占据了。   

优化
上面结果可以看出,这个程序大部分的时间都花在了CompareNodes函数上,用 grep 查看一下则发现CompareNodes 只是被CompareEdges调用了一次而已, 而CompareEdges则只被addAnnotatedEdge调用——它们都出现在了上面的清单中。这儿就是我们应该做点优化的地方了吧! 

我们注意到addAnnotatedEdge遍历了一个链表。虽然链表是易于实现,但是却实在不是最好的数据类型。我们决定将链表 g->edges 用二叉树来代替: 这将会使得查找更快。   

结果
现在我们看一下优化后的运行结果: 

real    2m19.314s
user    0m36.370s
sys     0m0.940s

  

第二遍
再次运行 gprof 来分析: 

%   cumulative self           self    total
 time   seconds seconds calls  s/call  s/call name
87.01     25.25  25.25  55000    0.00    0.00 getNode(char *,NodeListNode *&)
10.65     28.34   3.09  51987    0.00    0.00 addEdge(Graph *,Node *,Node *)

看起来以前占用大量运行时的函数现在已经不再是占用运行时的大头了!我们试一下再优化一下呢:用节点哈希表来取代节点树。 

这次简直是个巨大的进步: 

real    0m3.269s
user    0m0.830s
sys     0m0.090s


    gprof的输出信息

gprof的命令格式如下所示:
gprof OPTIONS EXECUTABLE-FILE gmon.out BB-DATA [YET-MORE-PROFILE-DATA-FILES...] [> OUTFILE]

gprof产生的信息含义如下所示:
%
time    the percentage of the total running time of the program used by this function.
函数使用时间占整个程序运行时间的百分比。
cumulative
seconds    a running sum of the number of seconds accounted for by this function and those listed above it. 
列表中包括该函数在内以上所有函数累计运行秒数。
self
seconds    the number of seconds accounted for by this function alone.  This is the major sort for this listing. 
函数本身所执行的秒数。
calls    the number of times this function was invoked, if this function is profiled, else blank.
函数被调用的次数
Self
ms/call    the average number of milliseconds spent in this function per call, if this function is profiled, else blank. 
每一次调用花费在函数的时间microseconds。
Total
ms/call    the average number of milliseconds spent in this function and its descendents per call, if this function is profiled, else blank. 
每一次调用,花费在函数及其衍生函数的平均时间microseconds。
name    the name of the function.  This is the minor sort for this listing. The index shows the location of the function in the gprof listing. If the index is in parenthesis it shows where it would appear in the gprof listing if it were to be printed.
函数名
    


    其他 C/C++ 程序分析器
 还有其他很多分析器可以使用gprof 的数据, 例如KProf (截屏) 和 cgprof。虽然图形界面的看起来更舒服,但我个人认为命令行的gprof 使用更方便。 

对其他语言的程序进行分析
我们这里介绍了用gprof 来对C/C++ 的程序进行分析,对其他语言其实一样可以做到: 对 Perl,我们可以用Devel::DProf 模块。你的程序应该以perl -d:DProf mycode.pl来开始,并使用dprofpp来查看并分析结果。如果你可以用gcj 来编译你的Java 程序,你也可以使用gprof,然而目前还只支持单线程的Java 代码。   

    结论
就像我们已经看到的,我们可以使用程序概要分析快速的找到一个程序里面值得优化的地方。在值得优化的地方优化,我们可以将一个程序的运行时从 3分36秒 减少到少于 5秒,就像从上面的例子看到的一样。   

    References
Pathalizer: http://pathalizer.sf.net

KProf: http://kprof.sf.net

cgprof: http://mvertes.free.fr

Devel::DProf http://www.perldoc.com/perl5.8.0/lib/Devel/DProf.html

gcj: http://gcc.gnu.org/java

: pathalizer example files: download for article371
(http://www.fanqiang.com)

原文链接:http://main.linuxfocus.org/ChineseGB/March2005/article371.shtml  




参考文献:

[1]   《掌握 Linux 调试技术》  Steve Best(sbest@us.ibm.com)JFS 核心小组成员,IBM 20028 月; 本文来源于IBM网站。
[2]  nfs.sourceforge.net 网站上的HOWTO,和 FAQ文档;
[3] 《building embedded linux systems》
[4] 《嵌入式linux系统开发详解--基于EP93XX系列ARM》
[5]  博客文章http://blog.sina.com.cn/u/1244756857
原文地址:https://www.cnblogs.com/timssd/p/4086208.html