Board logo

标题: 用 Graphviz 可视化函数调用(1) [打印本页]

作者: look_w    时间: 2018-5-9 20:29     标题: 用 Graphviz 可视化函数调用(1)

可以将以图形形式查看应用程序的调用过程看作是一个学习经历。这样做可以帮助您理解应用程序的内部行为,并获得有关程序优化方面的信息。例如,通过对那些经常调用的函数进行优化,您就可以用最少的努力来获得最佳的性能。另外,调用跟踪还可以判断用户函数的最大调用深度,这可以用来对调用栈使用的内存进行有效限制(在嵌入式系统中,这是非常重要的一个考虑因素)。
为了捕获并显示调用图,您需要 4 个元素:GNU 编译器工具链、Addr2line 工具、定制的中间代码和一个名为 Graphviz 的代码。Addr2line 工具可以识别函数、给定地址的源代码行数和可执行映像。定制的中间代码是一个非常简单的工具,它可以减少对图形规范的地址跟踪。Graphviz 工具可以生成图形映像。整个过程如图 1 所示。
图 1. 搜集、简化和可视化跟踪路径的过程数据搜集:捕获函数调用路径要收集一个函数调用的踪迹,您需要确定每个函数在应用程序中调用的时间。在过去,都是通过在函数的入口处和退出处插入一个惟一的符号来手工检测每个函数的。这个过程非常繁琐,而且很容易出错,通常需要对源代码进行大量的修改。
幸运的是,GNU 编译器工具链(也称为 gcc)提供了一种自动检测应用程序中的各个函数的方法。在执行应用程序时,就可以收集相关的分析数据。您只需要提供两个特殊的分析函数即可。其中一个函数在每次执行想要跟踪的函数时都会调用;而另外一个函数则在每次退出想要跟踪的函数时调用(参见清单 1)。这两个函数都是特别指定的,因此,编译器可以识别它们。
清单 1. GNU 的入口和出口配置函数
1
2
3
4
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. 分析函数
1
2
3
4
5
6
7
8
9
10
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 函数,则需要声明两个函数,然后对这两个函数应用 constructor 和 destructor 函数属性。在 constructor 函数中,会打开一个新的跟踪文件,分析数据的地址跟踪就是写入这个文件的;在 destructor 函数中,会关闭这个跟踪文件(参见清单 3)。
清单 3. 分析 constructor 和 destructor 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 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 工具。




欢迎光临 电子技术论坛_中国专业的电子工程师学习交流社区-中电网技术论坛 (http://bbs.eccn.com/) Powered by Discuz! 7.0.0