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

第7回 独自プロバイダの定義[2]

この記事を読むのに必要な時間:およそ 5 分

間接参照先の取得

間接参照先の内容を知りたいケースの最も典型的な例は,main()関数のargv引数から文字列を取得したい,といったようなケースでしょう。

DTrace動作原理に由来する制限

第2回で触れたように,DTraceは,指定されたDスクリプトをカーネル空間で実行しています。

そのため,関数起動時に指定された文字列引数を参照するためにはcopyinstr()サブルーチンを使用する必要がありました。

間接参照先の情報を取得する場合も,この動作原理の影響を受けます。

例として使用するmain()関数のargv引数の場合,引数の値そのものは,⁠"char*" の配列」の先頭アドレスに過ぎません。

つまり argvの値を直接使用した参照ができないのは勿論,argvの値を元に「"char*" の配列」をカーネル空間に copyinto しても,そこに格納されているのは「"char*"⁠⁠,つまりユーザ空間におけるアドレス値ですから,このままではその先にある文字列を参照することはできません。

間接参照先の取得

main() 関数のargv[0]が参照する文字列の取得を例に,間接参照先の情報を取得するためのD スクリプトをリスト6に示します。

リスト 6 argv[0] 文字列取得スクリプト (watch_argv0.d)

self uintptr_t* buf;

pid$target:$1:main:entry
{
    self->buf = alloca(sizeof(uintptr_t));
    copyinto(arg1, sizeof(uintptr_t), self->buf);
    printf("argv[0]='%s'", copyinstr(*(self->buf)));
}

copyinto()サブルーチンでカーネル空間に取り込んだアドレス情報self->bufに格納)を使用して,再度copyinstr()サブルーチンを実施することで,間接参照先の文字列argv[0] に相当)を取得しています。

Dスクリプト冒頭における"self uintptr_t* buf;" 記述は,self->bufの型を宣言しておくことで,スクリプト内で都度キャスト記述をせずに済ますためのものです。

DTraceは概ね自動的に型を判定してくれますが,必要に応じてリスト6のような適宜型宣言を行うことで,スクリプトの記述を簡素化することができます。

それでは実際に動かしてみましょう。

図3 argv[0] 文字列の採取

$ dtrace -s watch_argv0.d \
         -32 \
         -c '/usr/bin/true' \
         true
dtrace: script 'watch_argv0.d' matched 1 probe
dtrace: pid 911 has exited
CPU     ID                    FUNCTION:NAME
  0  60346                       main:entry argv[0]='/usr/bin/true'

$ 

採取対象に/usr/bin/trueコマンドを用いているのは,DTrace/D スクリプトによる出力と,コマンドの出力が混じって見辛くなるのを防ぐためですので,特に意味はありません。

ポインタ値ビット幅の問題

先ほどの実行例(図3)では,dtraceコマンド実行の際に,特に説明することなく"-32"オプションを使用しました。

実はこのオプションは,⁠ポインタ値のビット幅を32bitとして扱え」ということを指示するものです。

間接参照先の取得を行う場合,ポインタが格納されたユーザ空間の領域をカーネル空間へ複製する際と,複製した領域からのアドレス情報を取り出す際の両方で,ポインタ値のビット幅に依存した処理が必要になります。

Dスクリプトにおけるポインタ値のビット幅は,一般に "sizeof(intptr_t)" といった式を元に算出されますが,32bitアプリケーションを64bitカーネルで稼動させている場合(※2)には,アプリケーション側で想定している "sizeof(intptr_t)" 値と,カーネル側で想定している "sizeof(intptr_t)" 値が異なります。

そして,DTraceは基本的にカーネル側=ポインタ値ビット幅が64bitの立場でDスクリプトを解釈しますから,アプリケーション側=ポインタ値ビット幅を32bitとして動作しているユーザ空間との連携が適切に行えないのです。

そこで,Dスクリプトにおいて想定されているポインタ値ビット幅が32bitであることを"-32"オプションによって指定するわけです。

なお,VMWareやVirtualBox等の仮想化環境を使用する際には,32bitのホストOS上で32bit版の仮想化ソフトを使用しても,CPUが64bit対応の場合には64bitカーネルが稼動する場合がありますので,"isainfo -b"などによりカーネルの動作モードを確認することをお勧めします。

※2)
32bitアプリケーションを32bitカーネル上で,あるいは64bitアプリケーションを64bitカーネル上で稼動させている場合は,特に問題はありません。
しかし,64bitカーネル環境であっても,OSに同梱されているアプリケーションや,コンパイル時にデフォルトで生成される実行可能形式は,32bitアプリケーションであることが殆どです。

可変長間接参照への応用

main() 関数のargv[] 引数から任意の要素文字列を取得するような,可変長の間接参照に対しては,前ページでの可変長メモリのダンプにおける手法と組み合わせた,リスト7のような D スクリプトで対応することができます。

リスト7 argv[] 引数の任意個表示

self uintptr_t* argv;

pid$target:$1:main:entry
{
    self->index = -1;
}
pid$target:$1:main:entry
/(self->index += 1) < arg0/
{
    self->argv = alloca(sizeof(uintptr_t));
    copyinto(arg1 + (sizeof(uintptr_t) * self->index),
             sizeof(uintptr_t),
             self->argv);
    printf("argv[%d]='%s'", 
           self->index, copyinstr(*(self->argv)));
}
        :
    (以下,同じ記述の繰り返し)
        :

argv引数(=arg1)に対してsizeof(uintptr_t)self->index倍した値を加算することで,C/C++プログラム的に言うところのargv[self->index]に相当するアドレス値を算出しているため,copyinto()サブルーチンを使用する箇所が,少々ごちゃごちゃしてはいますが,これまでの説明を元にすれば十分理解できることと思います。

次回予告

次回は,ユーザプログラムに対してDTraceを適用する際に気になるであろう実行効率に関して,情報採取の実現方式を踏まえて説明したいと思います。

著者プロフィール

藤原克則(ふじわらかつのり)

Mercurial三昧の日々が嵩じて, いつの間にやら『入門Mercurial Linux/Windows対応』を上梓。凝り性なのが災いして,年がら年中アップアップな一介の実装屋。最近は仕事の縁が元で,OpenSolarisに入れ込む毎日。