以前受知乎用户mailto1587
启发,写了个C++
源码的调用图生成器,能够以图示法显示C++
函数的调用关系,
代码放在了github仓库里,仅供参考:
CodeSnippet/python/SRCGraphviz/c++ at master · Cheukyin/CodeSnippet · GitHubpython
利用gcc/g++
的-finstrument-functions
的注入选项,
获得每一个函数的调用地址信息,生成一个trace文件
,
而后利用addr2line
和c++filt
将函数名及其所在源码位置
从地址中解析出来,
从而获得程序的Call Stack
,
而后用pygraphviz
画出来c++
好比我如今有A.hpp
、B.hpp
、C.hpp
、ABCTest.cpp
这几个文件,
我想看他们的Call Graph
git
源码以下:github
而后按下面编译(instrument.c
在上面github
地址中能够下载,用于注入地址信息):g++ -g -finstrument-functions -O0 instrument.c ABCTest.cpp -o test
而后运行程序,获得trace.txt
输入shell
命令./test
最后
输入shell
命令python CallGraph.py trace.txt test
弹出一张Call Graph
shell
图上标注含义:bash
绿线表示程序启动后的第一次调用函数
红线表示进入当前上下文的最后一次调用this
每一条线表示一次调用,#
符号后面的数字是序号,at XXX
表示该次调用发生在这个文件(文件路径在框上方)的第几行spa
在圆圈里,XXX:YYY
,YYY
是调用的函数名,XXX
表示这个函数是在该文件的第几行被定义的prototype
利用-finstrument-functions
编译选项,
可让编译器在每一个函数的开头和结尾注入__cyg_profile_func_enter
和 __cyg_profile_func_exit
这两个函数的实现由用户定义
在本例中,只用到__cyg_profile_func_enter
,定义在instrument.c中,
其函数原型以下:void __cyg_profile_func_enter (void *this_fn, void *call_site);
其中this_fn
为 被调用的地址,call_site
为 调用方的地址
显然,假如咱们把全部的 调用方和被调用方的地址 都打印出来,
就能够获得一张完整的运行时Call Graph
所以,咱们的instrument.c实现以下:
/* Function prototypes with attributes */ void main_constructor( void ) __attribute__ ((no_instrument_function, constructor)); void main_destructor( void ) __attribute__ ((no_instrument_function, destructor)); void __cyg_profile_func_enter( void *, void * ) __attribute__ ((no_instrument_function)); void __cyg_profile_func_exit( void *, void * ) __attribute__ ((no_instrument_function)); static FILE *fp; void main_constructor( void ) { fp = fopen( "trace.txt", "w" ); if (fp == NULL) exit(-1); } void main_deconstructor( void ) { fclose( fp ); } void __cyg_profile_func_enter( void *this_fn, void *call_site ) { /* fprintf(fp, "E %p %p\n", (int *)this_fn, (int *)call_site); */ fprintf(fp, "%p %p\n", (int *)this_fn, (int *)call_site); }
其中main_constructor
在 调用main
前执行,main_deconstructor
在调用main
后执行,
以上几个函数的做用就是 将全部的 调用方和被调用方的地址 写入trace.txt
中
然而,如今有一个问题,就是trace.txt
中保存的是地址,咱们如何将地址翻译成源码中的符号?
答案就是用addr2line
以上面ABCTest.cpp
工程为例,好比咱们如今有地址0x400974
,输入如下命令addr2line 0x400aa4 -e a.out -f
结果为
_ZN1A4AOneEv /home/cheukyin/PersonalProjects/CodeSnippet/python/SRCGraphviz/c++/A.hpp:11
第一行该地址所在的函数名,第二行为函数所在的源码位置
然而,你必定会问,_ZN1A4AOneEv
是什么鬼?
为实现重载、命名空间等功能,所以C++
有name mangling
,所以函数名是不可读的
咱们须要利用c++filt
做进一步解析:
输入shell
命令 addr2line 0x400aa4 -e a.out -f | c++filt
结果是否是就清晰不少:
A::AOne() /home/cheukyin/PersonalProjects/CodeSnippet/python/SRCGraphviz/c++/A.hpp:11
注意这个结果中包含了函数名、函数所在文件和行号
通过上面的步骤,咱们已经能够把全部的(调用方, 被调用方)
对分析出来了,至关于获取到调用图全部的节点和边,
最后能够用pygraphviz
将 每一条调用关系 画出来便可,代码用python实如今 CallGraph.py 中