Callgraph

from : http://www.ibm.com/developerworks/cn/linux/l-graphvis/

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

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

幸运的是,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 __attribute__((__no_instrument_function__))
__cyg_profile_func_enter( void *this, void *callsite )
{
  /* Function Entry Address */
  fprintf(fp, "E%p\n", (int *)this);
}
void __attribute__((__no_instrument_function__))
 __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 );
}

gcc -finstrument-functions  test.c instrument.c -c
gcc -o test  test.o instrument.o
./test

there is a warning: incompatible implicit declaration of built-in function ‘exit’
Just ignore it.

 

除去某些函数,不想profile他们:

-finstrument-functions-exclude-file-list=file,file,...Set the list of functions that are excluded from instrumentation (see the description of -finstrument-functions). If the file that contains a function definition matches with one of file, then that function is not instrumented. The match is done on substrings: if the file parameter is a substring of the file name, it is considered to be a match.

For example, -finstrument-functions-exclude-file-list=/bits/stl,include/sys will exclude any inline function defined in files whose pathnames contain /bits/stl or include/sys.

If, for some reason, you want to include letter ',' in one of sym, write '\,'. For example, -finstrument-functions-exclude-file-list='\,\,tmp' (note the single quote surrounding the option).

-finstrument-functions-exclude-function-list=sym,sym,...This is similar to -finstrument-functions-exclude-file-list, but this option sets the list of function names to be excluded from instrumentation. The function name to be matched is its user-visible name, such as vector<int> blah(const vector<int> &), not the internal mangled name (e.g., _Z4blahRSt6vectorIiSaIiEE). The match is done on substrings: if the sym parameter is a substring of the function name, it is considered to be a match.

 

如果编译分析函数(在 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 -g -o test -Wl,-Map,test.map 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
$
Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s