(转)GCC 函式追蹤功能 finstrument-functions __attribute__ +用 Graphviz 可视化函数调用

目录(?)[-]

  1. January 09 2007
    1. GCC 函式追蹤功能
  2. 用 Graphviz 可视化函数调用
 
使用这两个网页可完成对一个自己的程序源码的框图的生成。
 
 

January 09, 2007

GCC 函式追蹤功能

昨天有一位同事問及 ARM call frame 相關的問題,我給的建議是透過 GCC Function instrumentation 的機制。該機制出現於 GCC 2.x,由 Cygnus (現為 RedHat) 所提出,在 GCC 中對應的選項為:"finstrument-functions",查看 man page 可得以下資訊:
  • -finstrument-functions

    Generate instrumentation calls for entry and exit to functions. Just after function entry and just before function exit, the following profiling functions will be called with the address of the current function and its call site. (On some platforms, "__builtin_return_address" does not work beyond the current function, so the call site information may not be available to the profiling functions otherwise.)
        void __cyg_profile_func_enter (void *this_fn,
                                       void *call_site);
        void __cyg_profile_func_exit  (void *this_fn,
                                       void *call_site);
    
    The first argument is the address of the start of the current function, which may be looked up exactly in the symbol table.

    This instrumentation is also done for functions expanded inline in other functions. The profiling calls will indicate where, conceptually, the inline function is entered and exited. This means that addressable versions of such functions must be available. If all your uses of a function are expanded inline, this may mean an additional expansion of code size. If you use extern inline in your C code, an addressable version of such functions must be provided. (This is normally the case anyways, but if you get lucky and the optimizer always expands the functions inline, you might have gotten away without providing static copies.)

    A function may be given the attribute "no_instrument_function", in which case this instrumentation will not be done. This can be used, for example, for the profiling functions listed above, high-priority interrupt routines, and any functions from which the profiling functions cannot safely be called (perhaps signal handlers, if the profiling routines generate output or allocate memory).
不過儘管有如此詳細的資訊,我們還是依循杜威博士的「作中學」方式來學習吧。首先,來寫個 "Hello World" 小程式: (hello.c)
#include <stdio.h>
#define DUMP(func, call) /
        printf("%s: func = %p, called by = %p/n", __FUNCTION__, func, call)

void __attribute__((__no_instrument_function__))
__cyg_profile_func_enter(void *this_func, void *call_site)
{
        DUMP(this_func, call_site);
}
void __attribute__((__no_instrument_function__))
__cyg_profile_func_exit(void *this_func, void *call_site)
{
        DUMP(this_func, call_site);
}

main()
{
        puts("Hello World!");
        return 0;
}
編譯與執行:
$ gcc -finstrument-functions hello.c -o hello
$ ./hello
__cyg_profile_func_enter: func = 0x8048468, called by = 0xb7e36ebc
Hello World!
__cyg_profile_func_exit: func = 0x8048468, called by = 0xb7e36ebc
看到 "__attribute__" 就知道一定是 GNU extension,而前述的 man page 也提到 -finstrument-functions 會在每次進入與退出函式前呼叫 "__cyg_profile_func_enter" 與 "__cyg_profile_func_exit" 這兩個 hook function。等等,「進入」與「退出」是又何解?C Programming Language 最經典之處在於,雖然沒有定義語言實做的方式,但實際上 function call 皆以 stack frame 的形式存在,去年在「深入淺出 Hello World」Part II 有提過。所以上述那一大段英文就是說,如果我們不透過 GCC 內建函式 "__builtin_return_address" 取得 caller 與 callee 相關的動態位址,那麼仍可透過 -finstrument-functions,讓 GCC 合成相關的處理指令,讓我們得以追蹤。而看到 __cyg 開頭的函式,就知道是來自 Cygnus 的貢獻,在 gcc 2.x 內部設計可瞥見不少。

當我們試著移除 "__attribute__((__no_instrument_function__))" 那兩行來看看: (wrong.c)
編譯與執行:
$ gcc -g -finstrument-functions wrong.c -o wrong
$ ./wrong
程式記憶體區段錯誤
發生什麼事情呢?請出 gdb 協助:
$ gdb ./wrong
GNU gdb 6.6-debian
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) run
Starting program: /home/jserv/HelloWorld/helloworld/instrument/wrong 

Program received signal SIGSEGV, Segmentation fault.
0x0804841d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7
7	{
(gdb) bt
#0  0x0804841d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7
#1  0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7
#2  0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7
...
#30 0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7
#31 0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7
#32 0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7
---Type  to continue, or q  to quit---
既然 "__cyg_profile_func_enter" 是 function hook,則本身不得也被施以 "function instrument",否則就無止盡遞迴了,不過我們也可以發現一件有趣的事情:
$ nm wrong | grep 8048414
08048414 T __cyg_profile_func_enter
如我們所見,"__cyg_profile_func_enter" 的位址被不斷代入 __cyg_profile_func_enter function arg 中。GNU binutils 裡面有個小工具 addr2line,我們可以該工具取得虛擬位址對應的行號或符號:
$ addr2line -e wrong 0x8048414
/home/jserv/HelloWorld/helloworld/instrument/wrong.c:7
就 Linux 應用程式開發來說,我們可透過這個機制作以下應用:
  • 撰寫特製的 profiler
  • 取得執行時期的 Call Graph
  • 置入自製的 signal handler,實做 backtrace 功能
  • 模擬 Reflection 機制
之前的 blog [透過 GCC 作 Call Graph 視覺化輸出] 提過一個透過 compiler code analysis 的方式,輸出 call graph 圖形的途徑,事實上,我們可透過 GCC "-finstrument-functions" 來作動態的分析處理,詳情可見 M. Tim Jones 的文章 [用 Graphviz 可視化函數調用],展示了如何整合這一系列的工具,達到構建一個動態的圖形函式呼叫調用生成。

撰寫特製的 profiler 的應用可見 [CygProfiler suite] 與 [FunctionCheck],那為何不用 binutils 中的 gprof 呢?因為很多時候系統會有缺陷,而 gprof 又依賴 kernel 與 glibc 的機制,當我們在移植新的系統時,就得用 "PoorMan's solution" (按:當然沒有 "PoorMan" 這家公司,這裡是強調「親手打造」之意) 來打穩底子。同樣的,「取得執行時期的 Call Graph」與「置入自製的 signal handler,實做 backtrace 功能」可參考知名的 [DirectFB] 計畫,當我們在編譯時期加入 "--enable-trace" (enable call tracing) 與 "--enable-debug" (enable debugging) 兩個選項時,編譯過程就會加入 GCC "-finstrument-functions" 選項,並且也提供特製的 signal handler 模擬 gdb backtrace 輸出功能。

又因為 "-finstrument-functions" 對系統偵錯是如此重要,我們來看看內部實做。我們好奇的是,main 函式中 puts("Hello World!") 呼叫的前後,到底被 GCC 施以什麼魔法呢?還是透過實驗的途徑:
$ objdump -d hello | grep -B2 puts
... (省略) ...
--
 8048488:	e8 87 ff ff ff       	call   8048414 <__cyg_profile_func_enter>
 804848d:	c7 04 24 df 85 04 08 	movl   $0x80485df,(%esp)
 8048494:	e8 c3 fe ff ff       	call   804835c 
看來 GCC 自動生成 __cyg_profile_func_enter 的 function call 動作,並置於 puts 呼叫之前,我們繼續觀察:
$ objdump -d hello | grep -A5 puts
... (省略) ...
--
 8048494:	e8 c3 fe ff ff       	call   804835c 
 8048499:	bb 00 00 00 00       	mov    $0x0,%ebx
 804849e:	8b 45 04             	mov    0x4(%ebp),%eax
 80484a1:	89 44 24 04          	mov    %eax,0x4(%esp)
 80484a5:	c7 04 24 68 84 04 08 	movl   $0x8048468,(%esp)
 80484ac:	e8 8d ff ff ff       	call   804843e <__cyg_profile_func_exit>
puts 呼叫之後,GCC 也自動生成 __cyg_profile_func_exit 的 function call 動作,這樣的技巧也被用於 KFT (Kernel Function Trace,前身為 KFI [Kernel function Instrumentation]),同樣以 GCC "-finstrument-functions" 來達到對每個 Linux Kernel function enter/exit 的追蹤功能,但得考慮大量的 static inline function。
由 jserv 發表於 January 9, 2007 04:59 PM
迴響

每次都覺得jserv大的文都像是變魔術...
很神

kornelius 發表於 January 9, 2007 09:53 PM


 
 
==========================================================================================================================================================

用 Graphviz 可视化函数调用

使用开源软件来简化复杂调用结构

developerWorks
 
 
将打印机的版面设置成横向打印模式

打印本页

 

 

将此页作为电子邮件发送

将此页作为电子邮件发送

 

 

 

 

级别: 初级

M. Tim Jones (mtj@mtjones.com), 资深软件工程师, Emulex

2005 年 7 月 11 日

花一些时间遍历一下源代码,可以向您展现所有的函数调用过程;但是如果函数指针非常复杂,或者代码太长且晦涩难懂,那么这个过程就可能更加困难了。本文将向您介绍如何使用开源软件和一些定制的代码来构建一个动态的图形函数调用生成器。

可以将以图形形式查看应用程序的调用过程看作是一个学习经历。这样做可以帮助您理解应用程序的内部行为,并获得有关程序优化方面的信息。例如,通过对那些经常调用的函数进行优化,您就可以用最少的努力来获得最佳的性能。另外,调用跟踪还可以判断用户函数的最大调用深度,这可以用来对调用栈使用的内存进行有效限制(在嵌入式系统中,这是非常重要的一个考虑因素)。

为了捕获并显示调用图,您需要 4 个元素:GNU 编译器工具链、Addr2line 工具、定制的中间代码和一个名为 Graphviz 的代码。Addr2line 工具可以识别函数、给定地址的源代码行数和可执行映像。定制的中间代码是一个非常简单的工具,它可以减少对图形规范的地址跟踪。Graphviz 工具可以生成图形映像。整个过程如图 1 所示。


图 1. 搜集、简化和可视化跟踪路径的过程
跟踪过程

数据搜集:捕获函数调用路径

要收集一个函数调用的踪迹,您需要确定每个函数在应用程序中调用的时间。在过去,都是通过在函数的入口处和退出处插入一个惟一的符号来手工检测每个函数的。这个过程非常繁琐,而且很容易出错,通常需要对源代码进行大量的修改。

幸运的是,GNU 编译器工具链(也称为 gcc)提供了一种自动检测应用程序中的各个函数的方法。在执行应用程序时,就可以收集相关的分析数据。您只需要提供两个特殊的分析函数即可。其中一个函数在每次执行想要跟踪的函数时都会调用;而另外一个函数则在每次退出想要跟踪的函数时调用(参见清单 1)。这两个函数都是特别指定的,因此,编译器可以识别它们。


清单 1. GNU 的入口和出口配置函数
				
void __cyg_profile_func_enter( void *func_address, void *call_site )
                                __attribute__ ((no_instrument_function));
void __cyg_profile_func_exit ( void *func_address, void *call_site )
                                __attribute__ ((no_instrument_function));

避免使用特殊的检测函数

您或许会产生疑惑,如果 gcc 就是我们需要的检测函数,那么为什么它不检测 __cyg_* 分析函数呢?gcc 的开发者曾思考过这个问题,他们提供了一个名为 no_instrument_function 的函数属性,这个函数属性可以应用于函数原型,禁止对它们进行检测。不要将这个函数属性应用到分析函数上,这样会导致无限递归分析循环和大量的无用数据。

在调用一个检测函数时,__cyg_profile_func_enter 同时也会被调用,并以 func_address 形式传递调用的函数地址,以及从中调用该函数的 call_site 形式的地址。反之,当一个函数退出时,也会调用 __cyg_profile_func_exit 函数,并传递 func_address 形式的函数地址,以及函数从中退出的真实地址,该地址的表示形式为 call_site

在这些分析函数中,您可以记录下地址对,以供以后再进行分析使用。要请求 gcc 所有的检测函数,每个文件都必须使用 -finstrument-functions-g 选项进行编译,这样可以保留调试符号。

因此,现在您就可以为 gcc 提供一些分析函数了,这些函数可以透明地插入应用程序中的函数入口点和函数退出点。但在调用分析函数时,又应该怎样处理所提供的地址呢?您有很多选择,但是为了简便起见,可以将这个地址简单地写入一个文件,要注意哪个地址是函数的入口地址,哪个地址是函数的出口地址(参见清单 2)。

注意:在清单 2 中并没有使用调用 Callsite 信息,因为这些信息对于分析程序来说是不必要的。


清单 2. 分析函数
				
void __cyg_profile_func_enter( void *this, void *callsite )
{
  /* Function Entry Address */
  fprintf(fp, "E%p/n", (int *)this);
}
void __cyg_profile_func_exit( void *this, void *callsite )
{
  /* Function Exit Address */
  fprintf(fp, "X%p/n", (int *)this);
}

现在您可以搜集分析数据了,但是您应该在什么地方打开或关闭您的跟踪输出文件呢?到现在为止,还不需要为了进行分析而对源程序进行任何修改。因此,您该如何检测整个应用程序(包括 main 函数)而不用对分析数据的输出结果进行初始化呢?gcc 的开发者也考虑过这个问题,它们为 main 函数的 constructor 函数和 destructor 函数提供了一些碰巧能够满足这个要求一些方法。constructor 函数是在调用 main 函数之前调用的,而 destructor 函数则是在应用程序退出时调用的。

要创建 constructor 和 destructor 函数,则需要声明两个函数,然后对这两个函数应用 constructordestructor 函数属性。在 constructor 函数中,会打开一个新的跟踪文件,分析数据的地址跟踪就是写入这个文件的;在 destructor 函数中,会关闭这个跟踪文件(参见清单 3)。


清单 3. 分析 constructor 和 destructor 函数
				
/* Constructor and Destructor Prototypes */
void main_constructor( void )
	__attribute__ ((no_instrument_function, constructor));
void main_destructor( void )
	__attribute__ ((no_instrument_function, destructor));
/* Output trace file pointer */
static FILE *fp;
void main_constructor( void )
{
  fp = fopen( "trace.txt", "w" );
  if (fp == NULL) exit(-1);
}
void main_deconstructor( void )
{
  fclose( fp );
}

如果编译分析函数(在 instrument.c)并将它们与目标应用程序链接在一起,然后再执行目标应用程序,结果会生成一个应用程序的调用追踪,追踪记录被写入 trace.txt 文件。跟踪文件与调用的应用程序处于相同的目录中。最终结果是,您可能会得到一个其中满是地址的非常大的文件。为了能够让这些数据更有意义,您可以使用一个不太出名的叫做 Addr2line 的 GNU 工具。




回页首


使用 Addr2line 将函数地址解析为函数名

Addr2line 工具(它是标准的 GNU Binutils 中的一部分)是一个可以将指令的地址和可执行映像转换成文件名、函数名和源代码行数的工具。这种功能对于将跟踪地址转换成更有意义的内容来说简直是太棒了。

要了解这个过程是怎样工作的,我们可以试验一个简单的交互式的例子。(我直接从 shell 中进行操作,因为这是最简单地展示这个过程的方法,如清单 4 所示。)这个示例 C 文件(test.c)是通过 cat 一个简单的应用程序实现的(也就是说,将标准输出的文本重定向到一个文件中)。然后使用 gcc 来编译这个文件,它会传递一些特殊的选项。首先,要(使用 -Wl 选项)通知链接器生成一个映像文件,并(使用 -g 选项)通知编译器生成调试符号。最终生成可执行文件 test。得到新的可执行应用程序之后,您就可以使用 grep 工具在映像文件中查找 main 来寻找它的地址了。使用这个地址和 Addr2line 工具,就可以判断出函数名(main)、源文件(/home/mtj/test/test.c)以及它在源文件中的行号(4)。

在调用 Addr2line 工具时,要使用 -e 选项来指定可执行映像是 test。通过使用 -f 选项,可以告诉工具输出函数名。


清单 4. addr2line 的一个交互式例子
				
$ cat >> test.c
#include <stdio.h>
int main()
{
  printf("Hello World/n");
  return 0;
}
<ctld-d>
$ gcc -Wl,-Map=test.map -g -o test test.c
$ grep main test.map
	0x08048258		__libc_start_main@@GLIBC_2.0
	0x08048258		main
$ addr2line 0x08048258 -e test -f
main
/home/mtj/test/test.c:4
$

Addr2line 和调试器

Addr2line 工具提供了基本的符号调试信息,不过 GNU Debugger (GDB)使用的是其他一些内部方法。




回页首


精简函数跟踪数据

现在您有了一个可以搜集函数函数地址的追踪数据的方法,还可以使用 Addr2line 工具将地址转换为函数名。然而,从应用程序中产生大量的跟踪数据之后,如何对这些数据进行精简,从而使其更有意义呢?这就是使用一些定制的中间代码在开源工具之间建立联系的地方。本文提供了这个工具(Pvtrace)的带有注释的完整代码,包括如何编译和使用该工具的一些说明。(有关的更多信息,请参阅 下载 一节。)

回想一下图 1 中的内容,在执行设置了检测函数的应用程序时,会创建一个名为 trace.txt 的文本文件。这个人们可以读取的文件中包含了一系列地址信息 —— 每行一个地址,每行都有一个前缀字符。如果前缀是 E,那么这个地址就是一个函数的入口地址(也就是说,您正在调用这个函数)。如果前缀是一个 X 字符,那么这个地址就是一个出口地址(也就是说,您正在从这个函数中退出)。

因此,如果在跟踪文件中有一个入口地址(A)紧跟着另外一个入口地址(B),那么您就可以推断是 A 调用了 B。如果一个入口地址(A)后面跟着一个出口地址(A),那么就说明这个函数(A)被调用后就直接返回了。当涉及大量的调用链时,就很难分析究竟是谁调用了谁,因此,一种简单的解决方案是维护一个整个地址的堆栈。每次在跟踪文件中碰到一个入口地址时,就将其压入堆栈。栈顶的地址就代表最后一次被调用的函数(也就是当前的活动函数)。如果后面紧接着是另外一个入口地址,这说明堆栈中的地址调用了这个刚从跟踪文件处读出的地址。在碰到退出函数时,当前的活动函数就会返回,并释放栈顶元素。这会将上下文返到回前一个函数,由此,就可以产生正确的调用链过程。

图 2 介绍了这个概念,以及精简数据的方法。在分析跟踪文件中的调用链时,会构建一个连通矩阵,用来表示哪个函数调用了其他哪些函数。这个矩阵的行表示调用函数的地址,列表示被调用的地址。对于每个调用对来说,行与列的交叉点不断进行累加(调用次数)。当处理完整个跟踪文件时,其结果是该应用程序的整个调用历史的一个非常简单的表示,其中包含了调用的次数。


图 2. 对跟踪数据进行处理和精简,并生成矩阵格式
精简过程
编译并安装工具

在下载并解压 Pvtrace 工具之后,只需在子目录中输入 make 命令,就可以编译 Pvtrace 工具了。也可以使用下面的代码将这个工具安装到 /usr/local/bin 目录中:

$ unzip pvtrace.zip -d pvtrace
$ cd pvtrace
$ make
$ make install

现在我们已经构建了简化的函数连通性矩阵,接下来应该构建图形的表示了。让我们深入研究 Graphviz,了便理解如何从连通矩阵生成一个调用图。




回页首


使用 Graphviz

Graphviz 或 Graph Visualization 是由 AT&T 开发的一个开源的图形可视化工具。它提供了多种画图能力,但是我们重点关注的是它使用 Dot 语言直连图的能力。在本文中,我们将简单介绍如何使用 Dot 来创建一个图形,并展示如何将分析数据转换成 Graphviz 可以使用的规范。(请参阅 参考资料 一节,以获得有关下载这个开源软件的信息。)

Dot 使用的图形规范

使用 Dot 语言,您可以指定三种对象:图、节点和边。为了让您理解这些对象的含义,我们将构建一个例子来展示这些元素的用法。

清单 5 给出了一个简单的定向图(directed graph),其中包含 3 个节点。第一行声明这个图为 G,并且声明了该图的类型(digraph)。接下来的三行代码用于创建该图的节点,这些节点分别名为 node1node2node3。节点是在它们的名字出现在图规范中时创建的。边是在在两个节点使用边操作(->)连接在一起时创建的,如第 6 行到第 8 行所示。我还对边使用了一个可选的属性 label,用它来表示边在图中的名称。最后,在第 9 行完成对该图规范的定义。


清单 5. 使用 Dot 符号表示的示例图(test.dot)
				
1:  digraph G {
2:    node1;
3:    node2;
4:    node3;
5:
6:    node1 -> node2 [label="edge_1_2"];
7:    node1 -> node3 [label="edge_1_3"];
8:    node2 -> node3 [label="edge_2_3"];
9:  }

要将这个 .dot 文件转换成一个图形映像,则需要使用 Dot 工具,这个工具是在 Graphviz 包中提供的。清单 6 介绍了这种转换。


清单 6. 使用 Dot 来创建 JPG 映像
				
$ dot -Tjpg test.dot -o test.jpg
$

在这段代码中,我告诉 Dot 使用 test.dot 图形规范,并生成一个 JPG 图像,将其保存在文件 test.jpg 中。所生成的图像如图 3 所示。在此处,我使用了 JPG 格式,但是 Dot 工具也可以支持其他格式,其中包括 GIF、PNG 和 postscript。


图 3. Dot 创建的示例图
Dot 创建的示例图

Dot 语言还可以支持其他一些选项,包括外形、颜色和很多属性。但是就我们想要实现的功能而言,这个选项就足够了。




回页首


综合

现在我们已经看到了整个过程的各个阶段了,下面可以采用一个例子来展示如何将这些阶段合并在一起了。现在,您应该已经展开并安装了 Pvtrace 工具,然后还需要将 instrument.c 文件复制到工作源代码目录中。

在这个例子中,我使用了一个源文件 test.c 进行检测。清单 7 给出了整个过程。在第 3 行中,我使用检测源(instrument.c)来构建(编译并连接)应用程序。然后在第 4 行执行 test,再使用 ls 命令验证已经生成了 trace.txt 文件。在第 8 行,我调用了 Pvtrace 工具,并提供这个映像文件作为它惟一的参数。映像名是必需的,这样 Addr2line(在 Pvtrace 中调用)就可以访问这个映像中的调试信息。在第 9 行中,我又执行了一次 ls 命令,以确保 Pvtrace 生成了 graph.dot 文件。最后,在第 12 行,使用 Dot 将这个图形规范转换成一个 JPG 图形映像。


清单 7. 创建调用跟踪图的整个过程
				
 1:  $ ls
 2:  instrument.c    test.c
 3:  $ gcc -g -finstrument-functions test.c -o test
 4:  $ ./test
 5:  $ ls
 6:  instrument.c     test.c
 7:  test             trace.txt
 8:  $ pvtrace test
 9:  $ ls
10:  graph.dot        test           trace.txt
11:  instrument.c     test.c
12:  $ dot -Tjpg graph.dot -o graph.jpg
13:  $ ls
14:  graph.dot        instrument.c   test.c
15:  graph.jpg        test           trace.txt
16:  $

这个过程的示例输出如图 4 所示。这个示例图是从使用 Q 学习的一个简单增强式学习应用程序中得到的。


图 4. 示例应用程序的跟踪结果
示例应用程序的跟踪结果

您也可以使用这种方法对更大的应用程序进行分析。我要展示的最后一个例子是 Gzip 工具。我简单地将 instrument.c 加入 Gzip 的 Makefile 中,作为其依赖的一个源文件,然后编译 Gzip,并使用它生成一个跟踪文件。这个图形太大了,不太容易进行更详细的分析,但是下图表示了 Gzip 对一个小文件进行压缩时的处理过程。


图 5. Gzip 跟踪结果
Gzip 跟踪结果



回页首


结束语

使用开源软件和少量的中间代码,只需要花很少的时间就可以开发出非常有用的项目。通过使用对应用程序进行分析的几个 GNU 编译器扩展,可以使用 Addr2line 工具进行地址转换,并对 Graphviz 应用程序进行图形可视化,然后您就可以得到一个程序,该程序可以对应用程序进行分析,并展示一个说明调用链的定向图。通过图形来查看一个应用程序的调用链对于理解应用程序的内部行为来说非常重要。在正确了解调用链及其各自的频率之后,这些知识可能对调试和优化应用程序非常有用。





回页首


下载

描述 名字 大小 下载方法
Instrumentation source and Pvtrace source pvtrace.zip 4 KB HTTP
关于下载方法的信息


参考资料

 

转调试技术--linux

一些有非常有用的调试技术。。

 

strings 可以用于去的调试信息的执行文件。列出里面的字符串常量等。如有时没有代码,不知程序里有没有打这个日志的时,可以方便用。

另外可以看到很多函数。。。

 

一、编译阶段

nm 获取二进制文件包含的符号信息

strings 获取二进制文件包含的字符串常量

strip 去除二进制文件包含的符号

readelf 显示目标文件详细信息

objdump 尽可能反汇编出源代码

addr2line 根据地址查找代码行

二、运行阶段

gdb 强大的调试工具

ldd 显示程序需要使用的动态库和实际使用的动态库

strace 跟踪程序当前的系统调用

ltrace 跟踪程序当前的库函数

time 查看程序执行时间、用户态时间、内核态时间

gprof 显示用户态各函数执行时间

valgrind 检查内存错误

mtrace 检查内存错误

三、其他

proc文件系统

系统日志

 

一、编译阶段nm(获取二进制文件里面包含的符号)

符号:函数、变量

参数:

-C 把C++函数签名转为可读形式

-A 列出符号名的时候同时显示来自于哪个文件。

-a 列出所有符号(这将会把调试符号也列出来。默认状态下调试符号不会被列出)

-l 列出符号在源代码中对应的行号(指定这个参数后,nm将利用调试信息找出文件名以及符号的行号。对于一个已定义符号,将会找出这个符号定义的行号,对于未定义符号,显示为空)

-n 根据符号的地址来排序(默认是按符号名称的字母顺序排序的)

-u 只列出未定义符号

 

strings(获取二进制文件里面的字符串常量)

功能:

获取二进制文件里面的字符串常量

用途:

比较重要的是检查KEY泄露

eg:strings <your_proc> | grep '^.\{16\}$‘ 查找<your_proc>中是否存在一行有16个字符的行,并显示出来。

选项:

-a不只是扫描目标文件初始化和装载段, 而是扫描整个文件。

-f在显示字符串之前先显示文件名。

-n min-len打印至少min-len字符长的字符串.默认的是4。

#strings /lib/tls/libc.so.6 | grep GLIBC

GLIBC_2.0

GLIBC_2.1

GLIBC_2.1.1

……

这样就能看到glibc支持的版本。

 

strip(去除二进制文件里面包含的符号)

用途:

可执行程序减肥(通常只在已经调试和测试过的生成模块上,因为不能调试了)

反编译、反跟踪

readelf(显示目标文件详细信息)

nm 程序可用于列举符号及其类型和值,但是,要更仔细地研究目标文件中这些命名段的内容,需要使用功能更强大的工具。其中两种功能强大的工具是objdump和readelf。

readelf工具使用来显示一个或多个ELF格式文件信息的GNU工具。使用不同的参数可以查看ELF文件不同的的信息。

readelf <option> <elffile>

-a 显示所有ELF文件的信息

-h 显示ELF文件的文件头

-l 显示程序头(program-header)和程序段(segment)和段下面的节

-S 显示较为详细的节信息(section)

-s 显示符号信息,

-n 显示标识信息(如果有)

-r 显示重定位信息(如果有)

-u 显示展开函数信息(如果有)

-d 显示动态节信息,一般是动态库的信息

 

objdump(尽可能反汇编出源代码)

objdump –S <exe>

尽可能反汇编出源代码,尤其当编译的时候指定了-g参数时,效果比较明显。

 

addr2line(根据地址查找代码行)

当某个进程崩溃时,日志文件(/var/log/messages)中就会给出附加的信息,包括程序终止原因、故障地址,以及包含程序状态字(PSW)、通用寄存器和访问寄存器的简要寄存器转储。

eg:Mar 31 11:34:28 l02 kernel: failing address: 0

如果可执行文件包括调试符号(带-g编译的),使用addr2line,可以确定哪一行代码导致了问题。

eg:addr2line –e exe addr

其实gdb也有这个功能,不过addr2line的好处是,很多时候,bug很难重现,我们手上只有一份crash log。这样就可以利用addr2line找到对应的代码行,很方便。

注意:

1. 该可执行程序用-g编译,使之带调试信息。

2. 如果crash在一个so里面,那addr2line不能直接给出代码行。

参数:

-a 在显示函数名或文件行号前显示地址

-b 指定二进制文件格式

-C 解析C++符号为用户级的名称,可指定解析样式

-e 指定二进制文件

-f 同时显示函数名称

-s 仅显示文件的基本名,而不是完整路径

-i 展开内联函数

-j 读取相对于指定节的偏移而不是绝对地址

-p 每个位置都在一行显示

 

二、运行阶段

调试程序的常见步骤:

1、确定运行时间主要花在用户态还是内核态(比较土的一个方法:程序暂时屏蔽daemon()调用,hardcode收到n个请求后exit(0),time一下程序……)。

2、如果是用户态,则使用gprof进行性能分析。

2‘、如果是内核态,则使用strace进行性能分析,另外可以使用其他工具(比如ltrace等)辅助。

 

ldd(显示程序需要使用的动态库和实际使用的动态库)# ldd /bin/ls

linux-gate.so.1 => (0xbfffe000)

librt.so.1 => /lib/librt.so.1 (0xb7f0a000)

libacl.so.1 => /lib/libacl.so.1 (0xb7f04000)

libc.so.6 => /lib/libc.so.6 (0xb7dc3000)

libpthread.so.0 => /lib/libpthread.so.0 (0xb7dab000)

/lib/ld-linux.so.2 (0xb7f1d000)

libattr.so.1 => /lib/libattr.so.1 (0xb7da6000)

第一栏:需要用什么库;第二栏:实际用哪个库文件;第三栏:库文件装载地址。

如果缺少动态库,就会没有第二栏。

 

strace(跟踪当前系统调用)

结果默认输出到2。

-p <pid> attach到一个进程

-c 最后统计各个system call的调用情况

-T 打印system call的调用时间

-t/-tt/-ttt 时间格式

-f/-F 跟踪由fork/vfork调用所产生的子进程

-o <file>,将strace的输出定向到file中。

如:strace -f -o ~/<result_file> <your_proc>

-e expr 指定一个表达式,用来控制如何跟踪,格式如下:

-e open等价于-e trace=open,表示只跟踪open调用

使用 strace –e open ./prg 来看程序使用了哪些配置文件或日志文件,很方便。

-e trace=<set> 只跟踪指定的系统调用

例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用.

-e trace=file只跟踪有关文件操作的系统调用

-e trace=process只跟踪有关进程控制的系统调用

-e trace=network跟踪与网络有关的所有系统调用

-e strace=signal 跟踪所有与系统信号有关的系统调用

-e trace=ipc跟踪所有与进程通讯有关的系统调用

 

ltrace(跟踪当前库函数)

参数和strace很接近

 

time(查看程序执行时间、用户态时间、内核态时间)

# time ps aux | grep 'hi'

1020 21804 0.0 0.0 1888 664 pts/6 S+ 17:46 0:00 grep hi

real 0m0.009s

user 0m0.000s

sys 0m0.004s

注意:

time只跟踪父进程,所以不能fork

 

gprof(显示用户态各函数执行时间)

gprof原理:

在编译和链接程序的时候(使用 -pg 编译和链接选项),gcc在你应用程序的每个函数中都加入了一个名为mcount(or“_mcount”, or“__mcount”)的函数,也就是说-pg编译的应用程序里的每一个函数都会调用mcount, 而mcount会在内存中保存一张函数调用图,并通过函数调用堆栈的形式查找子函数和父函数的地址。这张调用图也保存了所有与函数相关的调用时间,调用次数等等的所有信息。

使用步骤:

1、使用 -pg 编译和链接应用程序

gcc -pg -o exec exec.c

如果需要库函数调用情况:

gcc -lc_p -gp -o exec exec.c

2、执行应用程序使之生成供gprof 分析的数据gmon.out

3、使用gprof 程序分析应用程序生成的数据

gprof exec gmon.out > profile.txt

注意:

程序必须通过正常途径退出(exit()、main返回),kill无效。对后台常驻程序的调试——我的比较土方法是,屏蔽daemon()调用,程序hardcode收到n个请求后exit(0)。

有时不太准。

只管了用户态时间消耗,没有管内核态消耗。

 

gdb core exec (gdb查看core文件)

准备生成core:

启动程序前,ulimit -c unlimited,设置core文件不限制大小。(相反,ulimit -c 0,可以阻止生成core文件)

默认在可执行程序的路径,生成的是名字为core的文件,新的core会覆盖旧的。

设置core文件名字:

/proc/sys/kernel/core_uses_pid可以控制产生的core文件的文件名中是否添加pid作为扩展,1为扩展,否则为0。

proc/sys/kernel/core_pattern可以设置格式化的core文件保存位置或文件名,比如原来文件内容是core,可以修改为:

echo "/data/core/core-%e-%p-%t" > core_pattern

以下是参数列表:

%p - insert pid into filename 添加pid

%u - insert current uid into filename 添加当前uid

%g - insert current gid into filename 添加当前gid

%s - insert signal that caused the coredump into the filename 添加导致产生core的信号

%t - insert UNIX time that the coredump occurred into filename 添加core文件生成时的unix时间

%h - insert hostname where the coredump happened into filename 添加主机名

%e - insert coredumping executable name into filename 添加命令名

使用gdb查看core:

gdb <program> <core文件>

 

valgrind(检查内存错误)

使用步骤:

1、官网下载并安装valgrind。

2、-g编译的程序都可以使用。

官网的示例代码test.c

1 #include <stdlib.h>

2

3 void f(void)

4 {

5 int* x = malloc(10 * sizeof(int));

6 x[10] = 0; // problem 1: heap block overrun

7 } // problem 2: memory leak -- x not freed

8

9 int main(void)

10 {

11 f();

12 return 0;

13 }

编译程序gcc -Wall -g -o test test.c

3、valgrind启动程序,屏幕输出结果。

valgrind --tool=memcheck --leak-check=full ./test

注意:

valgrind只能查找堆内存的访问错误,对栈上的对象和静态对象没办法。

valgrind会影响进程性能,据说可能慢20倍,所以在性能要求高的情况下,只能使用mtrace这种轻量级的工具了(但是mtrace只能识别简单的内存错误)。

如果程序生成的core的堆栈是错乱的,那么基本上是stackoverflow了。这种情况,可以通过在编译的时候,加上 –fstack-protector-all 和 -D_FORTIFY_SOURCE=2来检测。Stack-protector-all 会在每个函数里加上堆栈保护的代码,并在堆栈上留上指纹。(记录下,没用过)

因为valgrind 查不了栈和静态对象的内存访问越界,这类问题,可以通过使用gcc的-fmudflap –lmudflap来检测。(记录下,没用过)

全局变量的类型不一致的问题,现在还找到比较好的方法,这从另一个方面说明全局对象不是个好的设计,这给调试带来了麻烦。

 

mtrace(检查内存错误)

mtrace是glibc內提供的工具,原理很简单,就是把你程序中malloc()和free()的位置全部下來,最后两辆配对,沒有配对到的就是memory leak。

使用的步骤如下:

1、代码中添加mtrace()

1 #include <stdio.h>

2 #include <stdlib.h>

3

4 int main(void)

5 {

6 int *p;

7 int i;

8

9 #ifdef DEBUG

10 setenv("MALLOC_TRACE", "./memleak.log", 1);

11 mtrace();

12 #endif

13

14 p=(int *)malloc(1000);

15

16 return 0;

17 }

这段代码malloc了一个空间,却沒有free掉。我们添加9-12行的mtrace调用。

2、编译gcc -g -DDEBUG -o test1 test1.c

3、执行./test1,在目录里会发现./memleak.log。

4、使用mtrace <your_proc> memleak.log查看信息。

# mtrace test1 memleak.log

- 0x0804a008 Free 3 was never alloc'd 0xb7e31cbe

- 0x0804a100 Free 4 was never alloc'd 0xb7ec3e3f

- 0x0804a120 Free 5 was never alloc'd 0xb7ec3e47

Memory not freed:

-----------------

Address Size Caller

0x0804a4a8 0x3e8 at /home/illidanliu/test1.c:14

可以看到test1.c 14行没有对应的free()。

 

三、其他proc文件系统

内核的窗口。

proc文件系统是一个伪文件系统,它存在内存当中,而不占用外存空间。

用户和应用程序可以通过proc得到系统的信息,并可以改变内核的某些参数。

proc/目录结构(部分):

cmdline 内核命令行

cpuinfo 关于Cpu信息

devices 可以用到的设备(块设备/字符设备)

filesystems 支持的文件系统

interrupts 中断的使用

ioports I/O端口的使用

kcore 内核核心映像

kmsg 内核消息

meminfo     内存信息

mounts 加载的文件系统

stat 全面统计状态表

swaps 对换空间的利用情况

version 内核版本

uptime 系统正常运行时间

net 网络信息

sys 可写,可以通过它来访问或修改内核的参数

 

proc/<pid>/目录结构(部分):

cmdline 命令行参数

environ 环境变量值

fd 一个包含所有文件描述符的目录

mem 进程的内存被利用情况

stat 进程状态

status Process status in human readable form

cwd 当前工作目录的链接

exe Link to the executable of this process

maps 内存映像

statm 进程内存状态信息

root 链接此进程的root目录

 

系统日志

/var/log/下的日志文件:

/var/log/messages 整体系统信息,其中也包含系统启动期间的日志。此外,mail、cron、daemon、kern和auth等内容也记录在var/log/messages日志中。

/var/log/auth.log 系统授权信息,包括用户登录和使用的权限机制等。

/var/log/boot.log 系统启动时的日志。

/var/log/daemon.log 各种系统后台守护进程日志信息。

/var/log/lastlog 记录所有用户的最近信息。这不是一个ASCII文件,因此需要用lastlog命令查看内容。

/var/log/user.log 记录所有等级用户信息的日志。

/var/log/cron 每当cron进程开始一个工作时,就会将相关信息记录在这个文件中。

/var/log/wtmp或utmp 登录信息。

/var/log/faillog 用户登录失败信息。此外,错误登录命令也会记录在本文件中。