前言
之前受知乎用戶mailto1587
啟發,寫了個C++
源碼的調用圖生成器,可以以圖示法顯示C++
函數的調用關系,
代碼放在了github倉庫里,僅供參考:
CodeSnippet/python/SRCGraphviz/c++ at master · Cheukyin/CodeSnippet · GitHub
主要思路
利用gcc/g++
的-finstrument-functions
的注入選項,
得到每個函數的調用地址信息,生成一個trace文件
,
然后利用addr2line
和c++filt
將函數名及其所在源碼位置
從地址中解析出來,
從而得到程序的Call Stack
,
然后用pygraphviz
畫出來
使用示例
比如我現在有A.hpp
、B.hpp
、C.hpp
、ABCTest.cpp
這幾個文件,
我想看他們的Call Graph
源碼如下:
然后按下面編譯(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
圖上標注含義:
- 綠線表示程序啟動后的第一次調用
- 紅線表示進入當前上下文的最后一次調用
- 每一條線表示一次調用,
#
符號后面的數字是序號,at XXX
表示該次調用發生在這個文件(文件路徑在框上方)的第幾行 - 在圓圈里,
XXX:YYY
,YYY
是調用的函數名,XXX
表示這個函數是在該文件的第幾行被定義的
獲取C/C++調用關系
利用-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 中