C/C++の関数の呼び出しをトレースする (GCC限定)
GCCの拡張機能である-finstrument-functionsオプションとLD_PRELOAD環境変数を利用して実行時に関数とメソッドの呼び出しをトレースする方法のメモです。
この方法の良いところは、他のツールと違って、自分の興味がある関数やメソッドがあるソースコードだけを対象にトレースできる所だと思います。dtraceやcallgrindだとシステムコールやライブラリの関数の呼び出しまでトレースしてしまったりします。関数呼び出しをトレースするお手軽な方法です。
手法
-finstrument-functionsオプションをつけてコンパイルすると関数やメソッドの入口と出口に自動的に関数を呼び出す命令を挿入します。入口で呼ばれる関数は__cyg_profile_func_enter、出口は__cyg_profile_func_exitです。そんな感じで関数を定義してコンパイルオプションを追加するだけで関数の呼び出しをトレース出来るんですけど、面倒なんでLD_PRELOAD環境変数を使ったテクニックとよく一緒に使われるみたいです
LD_PRELOAD環境変数に共有ライブラリを指定すると、プログラムを実行する前に指定された共有ライブラリが読み込まれるようになります。その時に共有ライブラリにある関数やメッソドのシンボルと実行ファイルのシンボルが被ってた場合は、先に読み込まれた共有ライブラリの関数やメソッドが利用される。
これを使ってフック関数を共有ライブラリ化してしまえばコンパイルオプションと環境変数を変えるだけで関数の呼び出しをトレースできるようになります。
サンプル
自分は以下のような感じで使ってます。
OSはUbuntu10.04で、gccのバージョンは4.4.3です。
test.cpp
// test.cpp
class B{
public:
B(){}
virtual ~B(){}
virtual void f(){}
};
class D : public B{
public:
D(){}
virtual void f(){}
};
void f1(int a){}
void f2(int b){}
int main(){
f1(0);
f2(0);
B *o = new D();
o->f();
delete o;
return 0;
}
trace.cpp
#include <dlfcn.h>
#include <cxxabi.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
extern "C" {
void __cyg_profile_func_enter(void* func_address, void* call_site);
void __cyg_profile_func_exit(void* func_address, void* call_site);
}
namespace ftracer {
const char* addr2name(void* address) {
Dl_info dli;
if (0 != dladdr(address, &dli)) {
return dli.dli_sname;
}
return NULL;
}
char* demangle(const char *symbol_name){
int status;
return abi::__cxa_demangle(symbol_name, 0, 0, &status);
}
void output(const char* action, void* func_address){
const char* symbol_name = addr2name(func_address);
if (symbol_name) {
char* func_name = demangle(symbol_name);
printf("%lu, %s, %s, %s\n", (unsigned long)clock(),
action, symbol_name, func_name ? func_name : "(null)");
free(func_name);
}
}
}
using ftracer::output;
void __cyg_profile_func_enter(void* func_address, void* call_site) {
output("enter", func_address);
}
void __cyg_profile_func_exit(void* func_address, void* call_site) {
output("exit", func_address);
}
コンパイルと実行
g++ -finstrument-functions -rdynamic test.cpp
LD_PRELOAD=./trace.so ./a.out
出力結果:
0, enter, main, (null)
0, enter, _Z2f1i, f1(int)
0, exit, _Z2f1i, f1(int)
0, enter, _Z2f2i, f2(int)
0, exit, _Z2f2i, f2(int)
0, enter, _ZN1DC1Ev, D::D()
0, enter, _ZN1BC2Ev, B::B()
0, exit, _ZN1BC2Ev, B::B()
0, exit, _ZN1DC1Ev, D::D()
0, enter, _ZN1D1fEv, D::f()
0, exit, _ZN1D1fEv, D::f()
0, enter, _ZN1DD0Ev, D::~D()
0, enter, _ZN1BD2Ev, B::~B()
0, exit, _ZN1BD2Ev, B::~B()
0, exit, _ZN1DD0Ev, D::~D()
0, exit, main, (null)
フック関数の引数で渡されるのは関数のアドレスだけです。アドレスを表示してnmコマンドとかで関数名を調べられますが、面倒なんでdladdrでシンボル名を取得して、abi::__cxa_demangleでシンボル名をdemangleしています。clock関数で実行時間も測っていますが一瞬で終わってるので全て0になってしまっています。
ちなみに、trace.cppでわざわざ新しい名前空間を作っているのはシンボル名が実行ファイルのシンボル名と被らないようにするためです。