アプリケーションのコール・トレースをグラフィカルな形で表示すると、さまざまなことがわかります。こうすると、アプリケーションの内部動作が理解しやすくなり、プログラムの最適化のための情報を得ることができます。たとえば、最も頻繁に呼び出される関数を最適化すれば、最小の労力で最大のパフォーマンス向上が得られます。また、コール・トレースによってユーザー関数の最大の呼び出し深さを調べることができ、それによってコール・スタックが使用するメモリーを効率的にバインドすることができます(組み込みシステムでは重要な考慮事項です)。
コール・グラフのキャプチャーと表示には、次の4つの要素が必要です。すなわち、GNUコンパイラー・ツールチェーン、Addr2lineユーティリティー、カスタム・グルー・コード、およびGraphvizという名前のツールです。Addr2lineユーティリティーを使用すると、特定のアドレスと実行可能イメージの関数およびソース行番号を調べることができます。カスタム・グルー・コードは、アドレス追跡をグラフ指定に縮小するシンプルなツールです。Graphvizツールは、グラフ・イメージを生成します。プロセス全体を図1に示します。
図1. トレースの収集、縮小、および視覚化プロセス
ファンクション・コール・トレースを収集するには、アプリケーション内でそれぞれの関数がいつ呼び出されるかを調べる必要があります。古き良き時代には、各関数を手動でインスツルメントして、関数の入口点と出口点のそれぞれで独自の記号を送ることによって、このタスクを行っていました。このプロセスは退屈で間違いが起きやすく、ソース・コードを煩雑にしがちでした。
幸い、GNUコンパイラー・ツールチェーン(gccとも呼ばれています)は、アプリケーションの望みの関数を自動的にインスツルメントする手段となります。インスツルメント対象のアプリケーションを実行すると、プロファイリング・データが収集されます。必要なのは、2つの特殊なプロファイリング関数を指定することだけです。1つは、インスツルメント対象の関数が呼び出されたときにディスパッチされます。もう1つは、インスツルメント対象の関数が終了するときに呼び出されます(リスト1)。これらの関数には、コンパイラーから識別できるように特殊な名前が付けられています。
リスト1. 入口と出口のGNUプロファイリング関数
void __cyg_profile_func_enter( void *func_address, void *call_site )
__attribute__ ((no_instrument_function));
void __cyg_profile_func_exit ( void *func_address, void *call_site )
__attribute__ ((no_instrument_function));
|
インスツルメント対象の関数が呼び出されると、__cyg_profile_func_enterも呼び出されて、呼び出された関数のアドレスをfunc_addressとして、また、関数の呼び出し元アドレスをcall_siteとして渡します。逆に、関数が終了すると、__cyg_profile_func_exit関数が呼び出されて、関数のアドレスをfunc_addressとして、関数が終了した実際のサイトをcall_siteとして渡します。
これらのプロファイリング関数でアドレスのペアを記録しておいて、後で分析することができます。gccにすべての関数をインスツルメントするように要求するには、デバッグ記号を保持するために、すべてのファイルを-finstrument-functionsおよび-gオプションを指定してコンパイルする必要があります。
これで、アプリケーションの関数の入口点と出口点にトランスペアレントに挿入するように、gccにプロファイリング関数を指定できるようになりました。しかし、プロファイリング関数が呼び出されたとき、与えられたアドレスをどうすればよいのでしょうか。いろいろなやり方がありますが、作業を単純にするには、どのアドレスが関数の入口で、どれが出口かをメモして、アドレスをファイルに書き込みます(リスト2)。
注:このプロファイリングアプリケーションでは不要なので、リスト2ではcallsite情報は使用されていません。
リスト2. プロファイリング関数
void __cyg_profile_func_enter( void *this, void *callsite )
{
/* Function Entry Address */
fprintf(fp, "E%p\n", (int *)this);
}
void __cyg_profile_func_exit( void *this, void *callsite )
{
/* Function Exit Address */
fprintf(fp, "X%p\n", (int *)this);
}
|
プロファイリング・データを収集できるようになりましたが、トレース出力ファイルのオープンとクローズはどこで行うのでしょうか。これまでのところ、プロファイリングのためにアプリケーションに変更を加える必要はありませんでした。では、プロファイリング・データ出力の初期設定をせずに、どのようにしてmain関数も含めたアプリケーション全体をインスツルメントするのでしょうか。gcc開発者たちは、この点も考慮して、このニーズに最適なmain関数コンストラクターとデストラクターのための手段を用意しました。コンストラクター関数は、mainが呼び出される直前に呼び出され、デストラクター関数はアプリケーション終了時に呼び出されます。
コンストラクターとデストラクターを作成するには、2つの関数を宣言して、それらにコンストラクターおよびデストラクター関数属性を適用します。コンストラクター関数では、新しいトレース・ファイルがオープンされて、そこにプロファイリング・アドレス・トレースが書き込まれます。デストラクター関数では、トレース・ファイルがクローズされます(リスト3)。
リスト3. プロファイリング・コンストラクターおよびデストラクター関数
/* Constructor and Destructor Prototypes */
void main_constructor( void )
__attribute__ ((no_instrument_function, constructor));
void main_destructor( void )
__attribute__ ((no_instrument_function, destructor));
/* Output trace file pointer */
static FILE *fp;
void main_constructor( void )
{
fp = fopen( "trace.txt", "w" );
if (fp == NULL) exit(-1);
}
void main_deconstructor( void )
{
fclose( fp );
}
|
プロファイリング関数(instrument.cにあります)がコンパイルされて、ターゲット・アプリケーションにリンクされ、そのアプリケーションが実行されると、アプリケーションのコール・トレースがtrace.txtファイルに書き込まれます。トレース・ファイルは、呼び出されたアプリケーションと同じディレクトリーに保存されます。結果として、大量のアドレスを含んだ大きなファイルができます。このデータを活用するには、Addr2lineという、あまり知られていないGNUユーティリティーを使います。
このプロセスがどのようなものかを見るために、簡単なインタラクティブな例を試してみましょう。(リスト4に示すように、プロセスのデモンストレーションを行うには、それが最も簡単な方法なので、私はシェルから直接操作しています。)サンプルCファイル(test.c)が、簡単なプログラムをcatすることによって(すなわち、テキストを標準入力からファイルにリダイレクトすることによって)作成されます。このファイルをgccでコンパイルし、いくつかの特殊なオプションを渡します。まず、リンカーにマップ・ファイルの生成を命じて(-W1オプション)、コンパイラーにデバッグ・シンボルの生成を命じます(-g)。この結果、実行可能ファイルtestが作成されます。新しい実行可能ファイルができたので、grepユーティリティーを使用してマップ・ファイルのmainを検索して、アドレスを見つけることができます。このアドレスと実行可能イメージ名をAddr2lineで使用して、関数名(main)、ソース・ファイル(/home/mtj/test/test.c)、およびソース・ファイル内の行番号(4)を調べます。
-eオプションで実行可能イメージをtestとして指定して、Addr2lineユーティリティーを起動します。-fオプションを使用して、関数名を出力するように指示します。
リスト4. インタラクティブなaddr2lineの例
$ cat >> test.c
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
<ctld-d>
$ gcc -Wl,-Map=test.map -g -o test test.c
$ grep main test.map
0x08048258 __libc_start_main@@GLIBC_2.0
0x08048258 main
$ addr2line 0x08048258 -e test -f
main
/home/mtj/test/test.c:4
$
|
これで、関数アドレス・トレースを収集して、Addr2lineユーティリティーでアドレスを関数名に解決する手段ができました。しかし、インスツルメント対象のアプリケーションから大量のトレース・アドレスを得ても、そのデータを意味のあるデータに縮小するにはどうすればよいのでしょうか。ここでカスタム・グルー・コードがオープン・ソース・ツール間の橋渡しをします。このユーティリティー(Pvtrace)のコメント付きソースと、ビルドおよび使用の説明が、この記事ととともに用意されています。(詳細については、「参考資料」を参照してください。)
図1で示したように、インスツルメント対象のアプリケーションが実行されると、trace.txtという名前のトレース・データ・ファイルが作成されます。この人間が読める形式のファイルにはアドレスのリストが含まれ、1行に1つずつのアドレスのそれぞれにプレフィックス文字が付いています。プレフィックスがEの場合、そのアドレスは関数入口アドレスです(すなわち、この関数が呼び出されました)。プレフィックスがXの場合、そのアドレスは出口アドレスです(すなわち、この関数は終了します)。
したがって、トレース・ファイル内に、入口アドレス(A)の後にもう1つの入口アドレス(B)があった場合は、AがBを呼び出したと考えることができます。入口アドレス(A)の後に出口アドレス(A)があった場合は、関数(A)が呼び出されて、戻ったと考えられます。呼び出しの連鎖が長くなると、どれがどれを呼び出したのかわかりにくくなりますが、それを解決する簡単な方法は、入口アドレスのスタックを保持することです。トレース・ファイルで入口アドレスが見つかるたびに、スタックにプッシュします。スタックの一番上のアドレスは、最後に呼び出された関数(すなわち、アクティブな関数)を表します。別の入口アドレスが続く場合は、スタック上のアドレスが、トレース・ファイルから最後に読み出されたアドレスを呼び出したことを意味します。出口アドレスが見つかったときには、現在のアクティブな関数が戻ったということなので、スタックの一番上の要素を廃棄します。これでコンテキストは前の関数、すなわち、呼び出しの連鎖の中の正しい流れに戻ります。
図2は、この概念とデータ縮小の方法を示しています。トレース・ファイルの呼び出しの連鎖が解析されるにつれて、どの関数がどの関数を呼び出したかを示す連結表が作成されます。表の行は呼び出し元アドレスを表し、列は呼び出し先アドレスを表します。呼び出しのペアごとに、それらが交差するセルが増分されます(コール・カウント)。トレース・ファイル全体が読み取られ、解析されると、コール・カウントも含めて、アプリケーションの呼び出し履歴全体をコンパクトな形で表す表ができあがります。
図2. トレース・データの解析と表形式への縮小
コンパクトな関数連結表ができたので、今度はこれをグラフにしましょう。Graphvizを探って、どのようにして連結表からコール・グラフが生成されるのかを理解しましょう。
Graphviz(Graph Visualization)は、AT&Tで開発されたオープン・ソースのグラフ視覚化ツールです。いくつかのグラフ機能を備えていますが、ここではDot言語を使用した有向グラフ機能に注目します。Dotでのグラフの作成を簡単に紹介して、プロファイリング・データをGraphvizで使用できる指定に変換する方法を説明します。(このオープン・ソース・パッケージのダウンロードについては、参考文献を参照してください。)
Dotによるグラフ指定Dot言語では、グラフ、ノード、およびエッジの3種類のオブジェクトを指定することができます。これらのオブジェクトが何を意味するのかを理解するために、3つの要素すべてを示す例を作成してみましょう。
リスト5は、Dot表記の3つのノードで構成される簡単な有向グラフを示しています。行1で、Gという名前のグラフとそのタイプ(digraph)を宣言しています。次の3行で、グラフのノード(node1、node2、node3)を作成します。ノードは、グラフ指定に名前が現れた時点で作成されます。エッジは、行6-8に示されているように、2つのノードがエッジ・オペレーター(->)によって結合されるときに作成されます。グラフ上のエッジに名前を付けるオプション属性labelも適用しました。最後に、行9でグラフ指定は完了です。
リスト5. Dot表記のサンプル・グラフ(test.dot)
1: digraph G {
2: node1;
3: node2;
4: node3;
5:
6: node1 -> node2 [label="edge_1_2"];
7: node1 -> node3 [label="edge_1_3"];
8: node2 -> node3 [label="edge_2_3"];
9: }
|
この.dotファイルをグラフ・イメージに変換するには、Graphvizパッケージに含まれているDotユーティリティーを使用します。リスト6に、この変換を示します。
リスト6. Dotを使用してJPGイメージを作成する
$ dot -Tjpg test.dot -o test.jpg
$
|
このコードでは、test.dotグラフ指定を使用して、test.jpgファイルにJPGイメージを生成するようにDotに指示しました。結果のイメージを図3に示します。私はJPG形式を使用しましたが、DotツールはGIF、PNG、postscriptなど、他の画像形式もサポートしています。
図3. Dotによって作成されたサンプル・グラフ
Dot言語は、形状や色などのオプションと多数の属性をサポートしています。しかし、ここでの目的では、このオプションだけで十分です。
プロセスのすべての断片を見たところで、1つの例でプロセスを実演してこれらを1つにまとめましょう。この時点で、すでにPvtraceユーティリティーの抽出とインストールは済んでいるはずです。また、instrument.cファイルを作業用のソース・ディレクトリーにコピーしてあるはずです。
この例では、test.cというソース・ファイルをインスツルメントします。リスト7にプロセス全体を示します。行3で、インスツルメンテーション・ソース(instrument.c)でアプリケーションをビルドします(コンパイルとリンク)。行4でtestを実行した後、lsユーティリティーを使用して、trace.txtファイルが生成されたことを確認します。行8で、Pvtraceユーティリティーを起動して、唯一の引数としてイメージ・ファイルを指定します。Addr2line(Pvtrace内から起動)がイメージ内のデバッグ情報にアクセスできるように、イメージの名前は必須です。行9で、もう一度lsを実行して、Pvtraceがgraph.dotファイルを生成したことを確認します。最後に、行12でDotを使用して、このグラフ指定をJPGグラフ・イメージに変換します。
リスト7. コール・トレース・グラフの作成プロセス全体
1: $ ls
2: instrument.c test.c
3: $ gcc -g -finstrument-functions test.c instrument.c -o test
4: $ ./test
5: $ ls
6: instrument.c test.c
7: test trace.txt
8: $ pvtrace test
9: $ ls
10: graph.dot test trace.txt
11: instrument.c test.c
12: $ dot -Tjpg graph.dot -o graph.jpg
13: $ ls
14: graph.dot instrument.c test.c
15: graph.jpg test trace.txt
16: $
|
プロセスのサンプル出力を図4に示します。このサンプル・グラフは、Q-learningを使用する単純な強化学習アプリケーションからのものです。
図4. サンプル・プログラムのトレース結果
この方法を使用して、はるかに大きなプログラムを表示することもできます。一例を挙げるならば、インスツルメントGzipユーティリティーです。GzipのMakefileで依存ファイルとしてinstrument.cを追加し、ビルドを行い、Gzipを使用すれば、トレース・ファイルが生成されます。この画像は大きすぎて細部がわかりにくいですが、このグラフはGzipが小さなファイルを圧縮するプロセスを表しています。
図5. Gzipのトレース結果
オープン・ソース・ソフトウェアと少量のグルー・コードによって、面白くて実用性もあるプロジェクトを短時間で開発することができます。アプリケーション・プロファイリングにはいくつかのGNUコンパイラー・エクステンションを使用し、アドレス変換にはAddr2lineユーティリティーを使用し、グラフの視覚化にはGraphvizプログラムを使用することによって、アプリケーションをプロファイルして呼び出しの連鎖を示す有向グラフを表示するプログラムを作ることができます。プログラムの呼び出しの連鎖をグラフィカルに表示することによって、そのプログラムの内部動作をよく理解することができます。この知識から、呼び出しの連鎖とそれぞれの頻度を理解して、アプリケーションのデバッグや最適化に役立てることができます。
- この記事で説明したインスツルメンテーションとPvtraceソースコードをダウンロードしてください。
- 最新のGCC(GNU Compiler Collection)ドキュメンテーションを見てください。
- gnu.orgにある
Addr2lineのメイン・ページで、Addr2lineユーティリティーについて、さらに学んでください。 - gcc.orgで、他のGNU Binutilsについても学んでください。
-
GraphvizのWebサイトで、他の可能性も探ってください。
-
Dotユーティリティーのマニュアルでは、このツールの他の機能についても概説しています。
-
Gzipユーティリティーと、その機能について学んでください。
- M. Tim Jonesによる
GNU/Linux Application Programming
を読んで、GNU/Linuxとオープンソース・ツールについて学んでください。
- 「Linuxのデバッグ手法をマスターする」(developerWorks, 2002年8月)は4つの問題シナリオを使って、デバッグ手法を概説しています。
- 「Kprobesによるカーネルのデバッグ」(developerWorks, 2004年8月)は、Linuxカーネルのデバッグの補助として、Kprobesを使って動的にprintkを挿入する方法を解説しています。
-
developerWorksのLinuxゾーンには、Linux開発者のための資料が他にも豊富に用意されています。
-
developerWorksdeveloperWorks blogsに参加して、developerWorksのコミュニティーに加わってください。
- Developer BookstoreのLinuxセクションでは、Linux関係の書籍が割り引きで購入できますので、ぜひご利用ください。
-
無料の2枚組DVDセット、SEK for Linuxをご注文ください。DB2RやLotusR、RationalR、TivoliR、WebSphereR など、Linux用の最新IBMソフトウェア試用版が含まれています。
- 皆さんの次期Linux開発プロジェクトを、IBM trial softwareを使って革新してください。developerWorksから直接ダウンロードすることができます。
