函数调用图 call-graph

前面的一些文章着重在静态的c代码分析。其中tags加上vim的一个插件可以比较方便的在代码间跳来跳去。可以列出你想要的函数,变量等。但是对于有函数指针的情况,或者是虚函数,函数间的调用就比较难追踪了。

ctags毕竟不是编译器,对c语言的了解只限于表层,并没有语义上的理解。

对c最了解的莫过于编译器,也就是gcc/g++了。 这里是gcc 在线文档

g++中有用的参数:
关于debug的参数,链接
-fdump-class-hierarchy
-fdump-tree
-fdump-translation-uni

这些参数会dump出一些gcc的内部信息,但是基本上没什么用,要有用得仔细去研究下其中的格式,还需要一些后处理才能整点有用的东西出来。

另外一些有用的参数是关于代码生成的参数,文档在这里 , 这里要着重介绍一个参数:
-finstrument-functions

添加了这个参数以后,编译器会在每个函数调用进入和退出时分别调用两个函数,这两个函数是:
void __cyg_profile_func_enter (void *this_fn,
void *call_site);
void __cyg_profile_func_exit (void *this_fn,
void *call_site);

如果你在你的可执行文件中实现了这两个函数(暂且叫它trace函数,或者跟踪函数),并且在编译的时候添加了这个参数,那么你的可执行文件运行时,就会把每次函数调用和退出记录下来(按照你定义的方式,即__cyg_profile_func_enter 和 __cyg_profile_func_exit的实现)。
这是一个非常不错的功能。

注意这里函数的参数,传入的参数是当前函数的地址和调用这个函数的地址。而不是函数名,要得到函数名,可以调用其他的函数来实现。
以下为一个简单的trace函数实现方法:

#include "stdio.h"
extern "C" {

void __cyg_profile_func_enter(void *this_fn, void *call_site)
                                  __attribute__((no_instrument_function));
void __cyg_profile_func_enter(void *this_fn, void *call_site) {
                       printf(fp,"E%p\n", this_fn);
            }
 } 

void __cyg_profile_func_exit(void *this_fn, void *call_site)
                                 __attribute__((no_instrument_function));
 void __cyg_profile_func_exit(void *this_fn, void *call_site) {
           printf(fp,"X%p\n", this_fn);
    }

}
void main_constructor( void )
        __attribute__ ((no_instrument_function, constructor));
void main_destructor( void )
         __attribute__ ((no_instrument_function, destructor));

void main_constructor( void )
{
      fp = fopen( "trace.txt", "w" );
        if (fp == NULL) printf("can not open trace.txt\n");
}


void main_deconstructor( void )
{
      if(fp) fclose( fp );
}

注意以上的代码,__cyg_profile_func_enter, __cyg_profile_func_exit 定义有属性为no_instrument_function , 这是给编译器看的,让编译器不要为这两个函数插入跟踪函数调用。 另外这两个函数要抱在extern “C” 中,告诉编译器,这两个函数的调用方法。

另外,如果想把输出打印到一个文件中,可以增加两个函数,main_constructor和main_deconstructor , 注意这两个函数的属性为no_instrument_function, constructor/destructor , 告诉编译器,不要插入跟踪函数调用,并且其中一个是构造函数,一个是析构函数。分别会在最先和最后调用。

对于打印出来的函数地址,在程序运行结束以后,可以利用addr2line来查询当前函数地址所对应的函数名以及在源码中的位置。具体的命令行为:
addr2line -e a.out -f -C 0xabcde00
其中-e指定可执行程序, -f 指定打印函数名, -C 指定输出的函数名为C风格的。最后是打印出来的地址。

—————————–
另外需要指出的是,如果给出的地址是某动态链接库中的地址,需要预先知道该动态链接库在运行时装载的基地址,然后用打印的地址减去基地址,然后用这个差值来调用addr2line。

如果你的__cyg_profile_func_enter, __cyg_profile_func_exit在动态链接库定义,并且被动态载入,那就可能不工作。原因是这两个跟踪函数在c的标准库中有一个缺省版本,但是这个缺省版本什么事情都不做。你调用跟踪函数的代码(在动态链接库中)在被装载的时候,跟踪函数被链接到标准库中的版本。直接的后果就是你的跟踪函数不会被调用。 要解决这个问题,可以把这两个跟踪函数单独定义在某个动态链接库中,然后让这个动态链接库在标准库之前被装入。这样可以保证你定义的函数会被首先调用。
举例说明:
你在test.so中定义了这两个函数,同时希望利用这两个函数来跟踪test.so中其他函数的调用关系。如果你的test.so是被动态装入的,那么你的test.so中其他函数在调用跟踪函数时,链接的跟踪函数实际上是glibc中的那个缺省版本,而不是你test.so中的版本。

要让你的库先于标准库载入,你需要在环境变量中定义LD_PRELOAD , 让这个变量指向你的动态链接库。
对于上面的情况就是,你得把这两个跟踪函数定义在另一个动态库比如说trace.so中,这个库中值包含这两个函数的定义。然后将LD_PRELOAD 设置为trace.so .这样你的test.so在被装载的时候,链入的trace函数就是trace.so中的版本而不是libc中的版本。

这样,所有的函数调用被导出后,可以利用后处理来生成函数调用图。方式是利用dot。
—————————————————
进一步补充, 在gcc4.3.6以后,引入另外两个重要的辅助参数:
-finstrument-functions-exclude-file-list=file,file,…
-finstrument-functions-exclude-function-list=sym,sym,…
可以指定哪些文件中的函数不需要添加跟踪函数,后者指定什么样的函数名中不需要调用跟踪函数。这是两个非常方便的参数。可以轻松过滤掉标准库函数和一些boost函数等等,使得我们跟踪的函数更有针对性。文档在这里



本文地址: http://www.bagualu.net/wordpress/archives/2297 转载请注明




发表评论

电子邮件地址不会被公开。 必填项已用*标注