C/C++プログラマのためのDTrace入門

第6回独自プロバイダの定義[1]

これまでの連載における説明では、基本的にpidプロバイダが提供するプローブentryおよびreturnを使用して、関数の呼び出し/復帰に関して情報を採取してきました。

しかし、実際のプログラム開発においては、情報を採取したい場所が、必ずしも関数呼び出しとは直接関係無い場合も多々あります。

そこで、今回と次回の2回に渡って、関数呼び出しの境界以外から情報を採取するための、独自プロバイダの定義とその利用について説明します。

今回は、単純な値を採取する独自プロバイダについて説明します。

ソースファイルの準備

以下の手順で必要となるソースファイルを準備します。

プロバイダの定義

何をおいても、まずは独自プロバイダを定義する必要があります。

リスト1 独自プロバイダの定義 (checkpoint.d)

provider checkpoint {
    pass(const char* filename, int lineno);
};

プロバイダcheckpointが提供するプローブpassは、以下のような用途を想定しています。

プログラムの要所要所に埋め込まれたプローブ位置において、ファイル名と行番号情報を採取することで、実行時にどのような経路を通過したのかを知る

つまり、関数フローよりもさらに細かい粒度で、実行フローを採取することができるわけです。

なおプロバイダ定義を記述する際には、ブロック末尾("{ }" の後ろ)にセミコロンが必要です。普段のDスクリプトでは不要な記述であることから、このセミコロンは忘れがちになりますので注意してください。

ヘッダファイルの生成

独自プロバイダを定義したならば、図1に示す要領でdtraceコマンドを実行してください。

図1 ヘッダファイルの生成 (checkpoint.h)

$ dtrace -s checkpoint.d -h

独自プロバイダを定義したDスクリプトの指定("-s" オプション)と共に、"-h" オプションを指定することで、リスト2のような内容を持ったヘッダファイルcheckpoint.hが生成されます。

リスト2 生成されたヘッダファイルの内容
          :
#define CHECKPOINT_PASS(arg0, arg1) \
        __dtrace_checkpoint___pass(arg0, arg1)
          :
          :
extern void __dtrace_checkpoint___pass(char *, int);
          :

"-h"オプション指定によってdtraceコマンドが生成するヘッダファイルでは、各プローブごとに「プロバイダ名」「プローブ名」の形式でマクロが定義されます(名称は共に大文字に変換されます⁠⁠。

通常は、ここで生成されたヘッダファイル中のマクロを使用して、対象ソースファイルにプローブの埋め込みを行います。

リスト2の例に見られるように、マクロそのものは別途宣言されている関数(この例では __dtrace_checkpoint___pass()を呼び出すだけの単純なものですが、名称の長さの点などからも、直接関数を呼ぶのではなくマクロを使う方が良いでしょう。

ユーティリティヘッダの定義

先述したように、通常はdtraceコマンドによって生成されたヘッダファイルと、そこで定義されているマクロを直接使いますが、今回のpassのようなプローブの場合、実装時におけるファイル名や行番号指定の手間を軽減するために、リスト3のようなマクロ定義を含むヘッダファイルcheckpoint_impl.hを定義しましょう。

リスト3 ユーティリティヘッダの作成 (checkpoint_impl.h)

#include "checkpoint.h"

#define DTRACE_CHECKPOINT_PASS() \
    CHECKPOINT_PASS(__FILE__, __LINE__)

CHECKPOINT_PASS()マクロを直接使用するのではなく、新たに定義したDTRACE_CHECKPOINT_PASS()マクロを使うことで、対象プログラムに対するプローブの埋め込みの手間が(幾分)軽減されます。

Cプログラムの実装

今回独自に定義したpassプローブの埋め込み対象として、リスト4に示す処理をmain()に持つプログラムを想定します。

リスト4 プローブ埋め込み対象プログラム (branch_by_arg.c)

    if(argc < 2){
        DTRACE_CHECKPOINT_PASS();
    }
    else{
        int val = atol(argv[1]);

        DTRACE_CHECKPOINT_PASS();

        if(val < 10){
            DTRACE_CHECKPOINT_PASS();
        }
        else{
            DTRACE_CHECKPOINT_PASS();
        }
    }

    DTRACE_CHECKPOINT_PASS();

この実装では与えられた引数に応じて条件分岐を行いますが、あくまで関数内に閉じた処理であり、他の関数を呼び出すわけでもありませんから、これまで使用してきたpidプロバイダによる関数フロー採取では、どのような経路が実行されたのかを知ることができません。

そこで、今回新たに定義したpassプローブを使用することで、どのファイルのどの行が実行されたのかを採取するわけです。

実行可能ファイルの生成

Cプログラムのコンパイル

プローブ埋め込み対象のCプログラム(前ページのリスト4)は、以下の要領でコンパイルします。

図2 プローブ埋め込み対象プログラムのコンパイル
$ cc -c  branch_by_arg.c

ここでのコンパイルにはDTraceの独自プローブ埋め込みの影響はありませんから、必要に応じて適宜オプション等を指定してください。

Cプログラムのリンク

コンパイルによって生成された、プローブ埋め込み対象のオブジェクトファイル(*.o ファイル)をリンクしようとすると、以下のようなエラーが発生します。

図3 プローブ埋め込み対象プログラムのリンク(失敗例)
$ cc -o branch_by_arg branch_by_arg.o
Undefined                       first referenced
 symbol                             in file
__dtrace_checkpoint___pass          branch_by_arg.o
ld: fatal: symbol referencing errors. \
    No output written to branch_by_arg
$

上記実行例で、⁠シンボル未定義」と判定された関数の呼び出しは、プローブ埋め込み用マクロcheckpoint.hにおけるCHECKPOINT_PASSマクロ)の展開によるものです(前ページのリスト2参照⁠⁠。

dtraceコマンドにより生成されたのはヘッダファイルのみですし、別途プローブ用に何かを実装したわけでもなければ、特別なライブラリをリンクしたわけでもありませんから、シンボル未定義となるのは至極当然の成り行きですね。

実はユーザプログラムに独自プローブを埋め込む場合、リンクに先立ってdtraceコマンドによる前処理を行う必要があります。

dtraceコマンドによるリンク前処理は、図4の要領で、"-G"オプションと、プローブが埋め込まれている全てのオブジェクトファイルを指定します。

図4 dtraceコマンドによるリンク前処理
$ dtrace -s checkpoint.d -G branch_by_arg.o

"-G"オプションが指定された場合、dtraceコマンドは以下の処理を実施します。

  1. 列挙されたオブジェクトファイルから、プローブ(="__dtrace_*"関数)呼び出しを除去
  2. 除去したプローブ呼び出しに関する情報をまとめ、それを格納したオブジェクトファイル(この場合はcheckpoint.oを生成

前処理が完了したなら、dtraceコマンドにより生成されたオブジェクトファイルを含めて、必要なオブジェクトファイルをリンクしてください。

図5 プローブ埋め込み対象プログラムのリンク(成功例)
$ cc -o branch_by_arg branch_by_arg.o checkpoint.o

リンク時オプションに関しても、DTraceの独自プローブ埋め込みの影響はありませんから、必要に応じて適宜オプション等を指定してください。

なお、dtrace コマンド+"-G"オプションによる前処理は、関連するオブジェクトファイル群の内容を改変してしまう点に注意してください。

一部のソースファイルを改変してから再度リンクを行う場合、ソースファイルを改変していないオブジェクトファイルに関しても、コンパイルによる再生成が必要となります。コンパイル~リンクまでの作業をmakeコマンド等で自動化する場合には、特に注意が必要です。

プローブからの情報採取

独自に定義したプロバイダからの情報採取の要領は、pidプロバイダの場合と同じ(※1)です。

リスト5に、checkpointプロバイダのpassプローブを使用するDスクリプトの例を示します。

リスト5 passプローブを使用するDスクリプト
checkpoint$target:$1::pass
{
    printf("%s:%d:%s\n", 
           copyinstr(arg0), arg1, probefunc);
}

それでは、さっそく情報採取をしてみましょう。

図6 passプローブからの情報採取
$ dtrace -s watch_checkpoint.d \
    -q \
    -c './branch_by_arg 13'
branch_by_arg.c:15:main
branch_by_arg.c:21:main
branch_by_arg.c:25:main

$

引数によって分岐した先でDTRACE_CHECKPOINT_PASS()マクロを通過するつど、ファイル名/行番号情報が出力されているのがわかります。

Emacsやviのエラー行ジャンプ機能と併用すれば、DTraceで採取した情報を元に処理経路順にソースを参照する、といったことも可能になります。

次回予告

今回は独自プロバイダの定義を行いましたが、いかがだったでしょうか?

定義/利用共に非常に簡単ですから、ぜひ実際のプログラミングに取り入れてみてください。

次回は、独自プロバイダ定義の後編ということで、プローブに指定された値から、より詳細な情報を得る方法を説明したいと思います。

おすすめ記事

記事・ニュース一覧