Graphvizによるファンクション・コールの視覚化

オープン・ソース・ソフトウェアによる複雑なコール構造の明確化

大量のソース・コードを時間をかけて見ていくと、関数の流れが見えてきますが、関数ポインターが関わってきたり、コードが長く入り組んでいたりすると、なかなか関数の流れが見えてきません。この記事では、オープン・ソース・ソフトウェアと少量のカスタム・グルー・コードを使用して、動的でグラフィカルなファンクション・コール・ジェネレーターを作成する方法を説明します。

M. Tim Jones, Consultant Engineer, Emulex

M. Tim JonesM. Tim Jones は、埋め込みソフトウェアのエンジニアであり、GNU/Linux Application Programming, AI Application Programming と BSD Sockets Programming from a Multilanguage Perspective の著者でもあります。エンジニアとして経歴は幅広く、静止衛星用のカーネル開発から埋め込みシステム・アーキテクチャー、そしてネットワーク・プロトコル開発まで経験しています。現在は Emulex Corp. のシニア主席エンジニアです。



2005年 6月 21日

アプリケーションのコール・トレースをグラフィカルな形で表示すると、さまざまなことがわかります。こうすると、アプリケーションの内部動作が理解しやすくなり、プログラムの最適化のための情報を得ることができます。たとえば、最も頻繁に呼び出される関数を最適化すれば、最小の労力で最大のパフォーマンス向上が得られます。また、コール・トレースによってユーザー関数の最大の呼び出し深さを調べることができ、それによってコール・スタックが使用するメモリーを効率的にバインドすることができます(組み込みシステムでは重要な考慮事項です)。

コール・グラフのキャプチャーと表示には、次の4つの要素が必要です。すなわち、GNUコンパイラー・ツールチェーン、Addr2lineユーティリティー、カスタム・グルー・コード、およびGraphvizという名前のツールです。Addr2lineユーティリティーを使用すると、特定のアドレスと実行可能イメージの関数およびソース行番号を調べることができます。カスタム・グルー・コードは、アドレス追跡をグラフ指定に縮小するシンプルなツールです。Graphvizツールは、グラフ・イメージを生成します。プロセス全体を図1に示します。

図1. トレースの収集、縮小、および視覚化プロセス
Trace process

データ収集:ファンクション・コール・トレースのキャプチャー

ファンクション・コール・トレースを収集するには、アプリケーション内でそれぞれの関数がいつ呼び出されるかを調べる必要があります。古き良き時代には、各関数を手動でインスツルメントして、関数の入口点と出口点のそれぞれで独自の記号を送ることによって、このタスクを行っていました。このプロセスは退屈で間違いが起きやすく、ソース・コードを煩雑にしがちでした。

幸い、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));

特定の関数のインスツルメンテーションを避ける
gccが関数をインスツルメントする場合、__cyg_* プロファイリング関数もインスツルメントするのではないかと疑問に思うことでしょう。gccの開発者たちはこの点を考慮して、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ユーティリティーを使います。


Addr2lineによる関数アドレスから関数名への解決

このプロセスがどのようなものかを見るために、簡単なインタラクティブな例を試してみましょう。(リスト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とデバッガー

Addr2lineユーティリティーは基本的なシンボリック・デバッガー情報を提供しますが、GNU Debugger(GDB)は内部的に他の方法を使用します。


関数トレース・データの縮小

これで、関数アドレス・トレースを収集して、Addr2lineユーティリティーでアドレスを関数名に解決する手段ができました。しかし、インスツルメント対象のアプリケーションから大量のトレース・アドレスを得ても、そのデータを意味のあるデータに縮小するにはどうすればよいのでしょうか。ここでカスタム・グルー・コードがオープン・ソース・ツール間の橋渡しをします。このユーティリティー(Pvtrace)のコメント付きソースと、ビルドおよび使用の説明が、この記事ととともに用意されています。(詳細については、「参考資料」を参照してください。)

図1で示したように、インスツルメント対象のアプリケーションが実行されると、trace.txtという名前のトレース・データ・ファイルが作成されます。この人間が読める形式のファイルにはアドレスのリストが含まれ、1行に1つずつのアドレスのそれぞれにプレフィックス文字が付いています。プレフィックスがEの場合、そのアドレスは関数入口アドレスです(すなわち、この関数が呼び出されました)。プレフィックスがXの場合、そのアドレスは出口アドレスです(すなわち、この関数は終了します)。

したがって、トレース・ファイル内に、入口アドレス(A)の後にもう1つの入口アドレス(B)があった場合は、AがBを呼び出したと考えることができます。入口アドレス(A)の後に出口アドレス(A)があった場合は、関数(A)が呼び出されて、戻ったと考えられます。呼び出しの連鎖が長くなると、どれがどれを呼び出したのかわかりにくくなりますが、それを解決する簡単な方法は、入口アドレスのスタックを保持することです。トレース・ファイルで入口アドレスが見つかるたびに、スタックにプッシュします。スタックの一番上のアドレスは、最後に呼び出された関数(すなわち、アクティブな関数)を表します。別の入口アドレスが続く場合は、スタック上のアドレスが、トレース・ファイルから最後に読み出されたアドレスを呼び出したことを意味します。出口アドレスが見つかったときには、現在のアクティブな関数が戻ったということなので、スタックの一番上の要素を廃棄します。これでコンテキストは前の関数、すなわち、呼び出しの連鎖の中の正しい流れに戻ります。

図2は、この概念とデータ縮小の方法を示しています。トレース・ファイルの呼び出しの連鎖が解析されるにつれて、どの関数がどの関数を呼び出したかを示す連結表が作成されます。表の行は呼び出し元アドレスを表し、列は呼び出し先アドレスを表します。呼び出しのペアごとに、それらが交差するセルが増分されます(コール・カウント)。トレース・ファイル全体が読み取られ、解析されると、コール・カウントも含めて、アプリケーションの呼び出し履歴全体をコンパクトな形で表す表ができあがります。

ツールの作成とインストール

Pvtraceユーティリティーをダウンロードして解凍したら、サブディレクトリーでmakeと入力するだけで、Pvtraceユーティリティーが作成されます。次のコードを使用して、/usr/local/binディレクトリーにインストールすることもできます。

$ unzip pvtrace.zip -d pvtrace
$ cd pvtrace
$ make
$ make install

図2. トレース・データの解析と表形式への縮小
図2. トレース・データの解析と表形式への縮小

コンパクトな関数連結表ができたので、今度はこれをグラフにしましょう。Graphvizを探って、どのようにして連結表からコール・グラフが生成されるのかを理解しましょう。


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によって作成されたサンプル・グラフ
図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. サンプル・プログラムのトレース結果
図4. サンプル・プログラムのトレース結果

この方法を使用して、はるかに大きなプログラムを表示することもできます。一例を挙げるならば、インスツルメントGzipユーティリティーです。GzipのMakefileで依存ファイルとしてinstrument.cを追加し、ビルドを行い、Gzipを使用すれば、トレース・ファイルが生成されます。この画像は大きすぎて細部がわかりにくいですが、このグラフはGzipが小さなファイルを圧縮するプロセスを表しています。

図5. Gzipのトレース結果
Gzip trace result

まとめ

オープン・ソース・ソフトウェアと少量のグルー・コードによって、面白くて実用性もあるプロジェクトを短時間で開発することができます。アプリケーション・プロファイリングにはいくつかのGNUコンパイラー・エクステンションを使用し、アドレス変換にはAddr2lineユーティリティーを使用し、グラフの視覚化にはGraphvizプログラムを使用することによって、アプリケーションをプロファイルして呼び出しの連鎖を示す有向グラフを表示するプログラムを作ることができます。プログラムの呼び出しの連鎖をグラフィカルに表示することによって、そのプログラムの内部動作をよく理解することができます。この知識から、呼び出しの連鎖とそれぞれの頻度を理解して、アプリケーションのデバッグや最適化に役立てることができます。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Open source
ArticleID=228574
ArticleTitle=Graphvizによるファンクション・コールの視覚化
publish-date=06212005