今回は、instrument-functions GNU拡張を使って、
プログラムを動的にトレース解析してくれるデバッガを実装していきます。
関数をトレースするツールのプログラム
まずはどのタイミングで、どの関数を呼び出されて、終了したかを追跡してくれるプログラムを作ってみましょう。
まずはプログラムのソースコードをみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
#include <dlfcn.h> /* dladdr */ #include <stdio.h> /* printf */ __attribute__((no_instrument_function)) const char *addr2name(void* address) { Dl_info dli; if (dladdr(address, &dli) != 0) { return dli.dli_sname; } else { return NULL; } } __attribute__((no_instrument_function)) void __cyg_profile_func_enter(void *addr, void *site_call) { const char *name = addr2name(addr); printf(">>> %s (%p)\n", name, addr); } __attribute__((no_instrument_function)) void __cyg_profile_func_exit(void *addr, void *site_call) { const char *name = addr2name(addr); printf("<<< %s (%p)\n", name, addr); } void foo(void); void bar(void); void baz(void); void hoge(void); int main(void) { foo(); foo(); return 0; } void foo(void) { bar(); baz(); } void bar(void) { } void baz(void) { } void hoge(void) { } |
このプログラムでは、普段使わないような見慣れない関数が記述されていると思うのですが、
__cyg_profile_func_enter()と__cyg_profile_func_exit()は、関数が呼び出されたとき、関数が終了したときに実行される関数です。
それぞれの関数の第一引数(addr)には呼び出された関数の情報が、第二引数(site_call)には呼び出した関数の情報が入っています。
また、addr2name()は関数名を返す関数です。
これらを使って関数をトレースしていくわけですが、
無限ループを防ぐために、__cyg_profile_func_enter()、__cyg_profile_func_exit()などには、
__attribute__((no_instrument_function))という記述を付け足します。
この記述が付け足された関数は、
『呼び出しと終了の際に__cyg_profile_func_enter()と__cyg_profile_func_exit()が呼び出され無くなる』
ということを意味します。
このプログラムを実行すると、
しっかりとトレースができていることが分かります。
>>>は関数の開始を、<<<は終了を表しています。
また一番右の()の中は関数アドレスを表します。
動的コールグラフを生成するプログラム
関数をトレースするプログラムを生成したので、
次は関数の主従関係を表す動的コールグラフを生成してみましょう。
まずはソースコードから。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
#include <stdarg.h> /* va_list */ #include <stdio.h> /* printf */ #include <dlfcn.h> /* dladdr */ #include <stdlib.h> /* atexit, getenv */ #include <string.h> /* strcmp, sprintf */ #define MAX_CALLS 1024 #define CALL_FUNC 0 #define CALLED_FUNC 1 const char *callGraph[MAX_CALLS][2]; int pairCount[MAX_CALLS]; int graphSize = 0; __attribute__((no_instrument_function)) const char *addr2name(void* address) { Dl_info dli; if (dladdr(address, &dli) != 0) { return dli.dli_sname; } else { return NULL; } } /* exit()の際に呼ばれる関数、グラフをファイルに書き出す */ __attribute__((no_instrument_function)) void program_exit() { int i; FILE *fp; fp = fopen("cg.dot", "w"); fputs("strict digraph G {\n", fp); for (i = 0; i < graphSize; i++) { char buf[100]; sprintf(buf, " %s -> %s [label=\"%d\"];\n", callGraph[i][CALL_FUNC], callGraph[i][CALLED_FUNC], pairCount[i]); fputs(buf, fp); } fputs("}", fp); fclose(fp); } /* 関数開始時 */ __attribute__((no_instrument_function)) void __cyg_profile_func_enter(void *addr, void *call_site) { const char *call_func = addr2name(call_site), *called_func = addr2name(addr); int i, flag = 1; /* プログラム開始時(start から main の呼び出し時)にatexit()の設定をする */ if (strcmp(call_func, "start") == 0) { atexit(program_exit); for (i = 0; i < MAX_CALLS; i++) pairCount[i] = 1; } /* グラフの作成 */ if (strcmp(call_func, "start") != 0 && graphSize < MAX_CALLS) { /* 同種の対があるかチェック */ for (i = 0; i < graphSize; i++) { if (strcmp(callGraph[i][CALL_FUNC], call_func) == 0 && strcmp(callGraph[i][CALLED_FUNC], called_func) == 0) { pairCount[i]++; flag = 0; } } if (flag) { callGraph[graphSize][CALL_FUNC] = call_func; callGraph[graphSize][CALLED_FUNC] = called_func; graphSize++; } } } /* 関数終了時 */ __attribute__((no_instrument_function)) void __cyg_profile_func_exit(void *addr, void *call_site) { } void foo(void); void bar(void); void baz(void); void qux(void); void hoge(void); int main(void) { foo(); foo(); hoge(); return 0; } void hoge(void) { foo(); } void foo(void) { bar(); baz(); } void bar(void) { } void baz(void) { } void qux(void) { } |
このプログラムでは関数が呼ばれたときに、
__cyg_profile_func_enter()で呼ばれた関数名を登録するようにしています。
その際にmain関数が呼び出された場合(プログラムの開始時)に限って、
cg.dotファイルに有効グラフを記述するprogram_exit()をatexit()を使ってプログラム終了時に呼び出すように設定しています。
このプログラムを実行すると、以下のような内容のcg.dotというファイルが生成されます。
1 2 3 4 5 6 7 8 9 |
strict digraph G { main -> foo [label="2"]; foo -> bar [label="2"]; foo -> baz [label="2"]; baz -> qux [label="2"]; main -> hoge [label="1"]; } |
これはGraphViz形式で表された動的コールグラフで、dot命令でPNGやSVGに変換することができます。
これを、
1 2 3 |
dot -Tpng cg.dot > cg.png |
で実行すると、以下のPNG画像が表示されます。
(もしGraphVizをインストールしていない人は、先にインストールを済ませておいてください。)
生成される画像は呼び出した関数から呼び出された関数への有向グラフになっています。
また、ラベルの数字は、呼び出し元が何回だけその関数を呼び出したかを表しています。
このプログラムを使うと、関数の主従関係が視覚的に分かるようになります。