C/C++プログラム実行時の関数をトレースする方法
C/C++プログラム実行時の関数をトレースする方法を紹介します。
1.はじめに
C/C++プログラムで実行時の関数をトレースする場合、printf()などを埋め込んでログに出力することが少なくないと思います。
が、その方法ではログ出力するためにプログラムに手を入れなくてはならなず、非効率です。
また規模の大きなプログラムでは現実的な解決方法ではありません。
printf()を埋め込まずにトレースできないか、方法を探していたところ、標準でそのような機能があることをみつけました。
仕組みは、gccでトレースしたいプログラムファイルのコンパイル時に、コンパイルオプション"-finstrument-functions"を付与することで、関数の実行開始時および復帰時に下記のフック関数を呼び出せるようになります。
void __cyg_profile_func_enter(void* func, void* caller);
void __cyg_profile_func_exit(void* func, void* caller);
あとはこの2つの関数にログ出力を実装し、ライブラリとして引き込めばOKです。
ということで、C/C++プログラムを動かして関数をトレースする方法を紹介します。
2.ソースコード
まず、トレース用の関数(trace.cpp)です。
前述の__cyg_profile_func_enter()、__cyg_profile_func_exit()を実装し、トレース情報を出力します。
trace.cpp
#include <dlfcn.h>
#include <iostream>
extern "C" {
void __cyg_profile_func_enter(void* func, void* caller);
void __cyg_profile_func_exit(void* func, void* caller);
}
const char* to_name(void* address) {
Dl_info dli;
if (0 != dladdr(address, &dli)) {
return dli.dli_sname;
}
return 0;
}
void __cyg_profile_func_enter(void* func, void* caller) {
const char* name = to_name(func);
if (name) {
std::cout << "start:" << name << std::endl;
} else {
std::cout << "start:" << func << std::endl;
}
}
void __cyg_profile_func_exit(void* func, void* caller) {
const char* name = to_name(func);
if (name) {
std::cout << "end :" << name << std::endl;
} else {
std::cout << "end :" << func << std::endl;
}
}
そしてトレース対象となるC++サンプル関数(sample.cpp/sample.c)です。
サンプルでは、main()→hello_world1()→hello_world2()の順に起動するようにします。
ファイルの内容は同じですが、C用とC++用に異なるファイル名で2つ作成します。
sample.cpp/sample.c
#include <stdio.h>
void hello_world2(void) {
printf( "Hello World2!\n" ); }
void hello_world1(void) {
printf( "Hello World1!\n" );
hello_world2();
}
int main(void) {
hello_world1();
return 0;
}
3.コンパイル
トレース用関数(trace.cpp)
% g++ -fPIC -shared trace.cpp -o libctrace.so -ldl
トレース対象のC++サンプル関数(sample.cpp)
% g++ -fPIC -finstrument-functions sample.cpp -o helloworld_cpp
トレース対象のC++サンプル関数(sample.c)
% gcc -fPIC -finstrument-functions sample.c -o helloworld_c
これで下記のとおり、サンプルのhelloworld_c、helloworld_cppと、トレース用ライブラリlibctrace.soが作られます。
% ls -l
-rwxr-xr-x. 1 hoge hoge 7327 1月 17 23:21 2017 helloworld_c
-rwxr-xr-x. 1 hoge hoge 8256 1月 17 23:30 2017 helloworld_cpp
-rwxr-xr-x. 1 hoge hoge 8498 1月 17 23:23 2017 libctrace.so
-rw-r--r--. 1 hoge hoge 342 1月 17 23:20 2017 sample.c
-rw-r--r--. 1 hoge hoge 342 1月 17 23:12 2017 sample.cpp
-rw-r--r--. 1 hoge hoge 784 1月 17 23:23 2017 trace.cpp
4.実行
実行時は環境変数"LD_PRELOAD"を設定します。
LD_PRELOADにlibctrace.soを指定することでこのライブラリが最優先で読み込まれ、__cyg_profile_func_enter()および__cyg_profile_func_exit()が実行されます。
helloworld_cpp(C++)の実行
% LD_PRELOAD=./libctrace.so ./helloworld_cpp | c++filt
start:main
start:hello_world1()
Hello World1!
start:hello_world2()
Hello World2!
end :hello_world2()
end :hello_world1()
end :main
c++filtはC++やJavaのシンボルをデマングルするコマンドです。
helloworld_cpp(C++)でc++filtを使わずに実行
% LD_PRELOAD=./libctrace.so ./helloworld_cpp
start:main
start:_Z12hello_world1v
Hello World1!
start:_Z12hello_world2v
Hello World2!
end :_Z12hello_world2v
end :_Z12hello_world1v
end :main
helloworld_c(C)の実行
% LD_PRELOAD=./libctrace.so ./helloworld_c
start:main
start:hello_world1
Hello World1!
start:hello_world2
Hello World2!
end :hello_world2
end :hello_world1
end :main
なお、実行環境によってトレース関数内で使っているdladdr()が正常に動作しない(=アドレスから関数名が取得できない)ケースがありました。
また、トレース用ライブラリのコンパイルで"-ldl"を指定しないと、実行時に下記のエラーが発生することを確認しています。
./helloworld_cpp: symbol lookup error: ./libctrace.so: undefined symbol: dladdr
5.参考サイト
参考サイトは下記です。ありがとうございました。
- 実行時の関数全ての in/out をトレースする
- 第14回 GCC2.95から追加変更のあったオプションの補足と検証(その3)
- LD_PRELOADで動的ライブラリ関数を上書きする
- FEATURE_TEST_MACROS
- feature_test_macros - 約束事その他の説明 - Linux コマンド集 一覧表
- DLOPEN
- 自作の関数に対して、簡易コールグラフを出力する。