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

第2回関数引数/戻り値の採取

前回はDTraceを用いて関数呼び出しフローを採取しました。今回は、関数呼び出しにおける、より詳細な情報の採取を行います。

引数値の採取

関数呼び出しにおいて採取したい情報の筆頭は、何と言っても引数に関する情報でしょう。

引数値の表示

まずは、以下の様な関数 multiply() を持つプログラムshow_args を想定します。

リスト1 採取対象関数multiply()
int
multiply(int x, int y)
{
    return (x * y);
}

この関数が呼び出された際の引数を表示するDスクリプトは、以下のように記述します(自前のプログラムに対して試す場合は、show_argsおよびmultiply部分を適宜書き換えてください⁠⁠。

リスト2 引数値採取Dスクリプト
pid$target:show_args:multiply:entry
{
    printf("multiply(%d, %d)\n", arg0, arg1);
}

上記Dスクリプトの各要素は、以下の様な意味を持ちます("pid$target:show_args" および"entry"部分に関しては、前回で解説済みですので割愛します⁠⁠。

表1 Dスクリプトの構成要素
multiply この位置には情報採取対象の関数名を記述します。
この例では、multiply関数が採取対象となります。
printf() C 標準ライブラリの printf() 関数に相当する処理を行う DTraceアクションを呼び出します。
標準Cライブラリで既定されているフォーマット指定は概ね使用可能です。
arg0, arg1 (この例では)関数呼び出しにおける引数を参照するのに使用します。
最初の引数が arg0以降の引数が順に arg1arg2 ...で参照可能です。
範囲外の引数を参照した場合の値は不定となります。

dtrace コマンドの起動方法は前回と同様です。

図1 引数値の採取
$ dtrace -s watch_arg_val.d \
         -q \
         -c './show_args 13 17'
multiply(13, 17) ← 採取結果
$ 

今回は関数フローの必要が無いので、-F 指定を省略しています。

また -q 指定により、DTraceによる関数名やCPU ID等の付加情報表示を抑止して、D スクリプトのprintf() アクションによる)明示的な出力指定のみを表示するようにしています。

D スクリプトの文法詳細

これまでは、個々の状況におけるDスクリプトの説明をしてきましたが、ここで改めてDスクリプトの文法について説明したいと思います。

D スクリプトは、以下の構造を1つ、ないし複数記述したものです(※1⁠⁠。

リスト3 Dスクリプト文法
<Probe-Description> [, ....]
{
    [<Action> ....]
}

<Action> 部分は、先の例で記述した printf() のような処理を、セミコロン区切りで複数列挙することができます。

<Probe-Description>は以下の形式で構成されます。

リスト4 <Probe-Description> の構成
<Provider>:<ProbeModule>:<ProbeFunc>:<ProbeName>

各要素は以下の意味を持ちます。

表2 <Probe-Description> の構成要素
<Provider> 情報採取機能の種類を指定します。この機能種別を DTrace ではプロバイダ(provider)と呼びます。
ユーザプログラムからの情報採取の場合は、専ら pidプロバイダの使用がメインになると思いますが、DTraceは他にも多数のプロバイダを用意しています。
<ProbeModule> 情報採取対象のバイナリファイル名を指定します。
現状は採取対象コマンド名と同一とみなして構いません(応用方法に関しては、第3回で説明する予定です⁠⁠。
<ProbeFunc> 情報採取対象となる関数名を指定します。
<ProbeName> どの時点で情報を採取するかを指定します。
pid プロバイダを使用する場合、entry(関数の開始)およびreturn(関数の終了)を指定可能です。

上記の各要素の組み合わせによって特定される「情報採取対象」のことを、DTrace ではプローブ(probe)と呼びます。

<ProbeModule><ProbeFunc> が採取対象となる関数(=範囲)を、<Provider><ProbeName> が採取対象となる情報(=種別)を絞り込みます。

<Probe-Description> 中の各要素は省略可能で、省略された場合は「選択可能な全て」が指定されたものとみなされます。

ただし、pidプロバイダを使用してユーザプログラムから情報採取する場合、実質的に省略可能な要素は<ProbeFunc>ぐらいだと思ってください(※2⁠⁠。

<Action> が実行される際の採取対象プローブに関する<Probe-Description> 中の各要素は、Dスクリプト中からそれぞれprobeprovprobemodprobefunc および probenameという組み込み変数を使って参照することができます。

そのため、(1) <Probe-Description>はカンマで区切って複数列挙できることと、(2) 現時点で <Action> の対象となっている関数名をprobefunc参照できることを利用して:

リスト5 <Action>共有例
pid$target:show_args:add:entry,
pid$target:show_args:subtract:entry,
pid$target:show_args:multiply:entry,
pid$target:show_args:divide:entry
{
    printf("%s(%d, %d)\n", probefunc, arg0, arg1);
}

上記のように、1つの<Action>を、複数の関数に対する<Probe-Description>で共有しつつ、実際に呼び出された関数の名前を表示内容に含めることが可能です。

上記以外の組み込み変数に関しては、Solaris動的トレースガイドの組み込み変数の解説を参照してください。

引数文字列の採取

文字列表示に関する制限

関数呼び出しにおける引数値表示の次は、文字列引数の内容を表示してみましょう。

show_args コマンドは、main()関数のargv[0]引数、つまり自身の名前を使って以下の関数shownameを呼び出すものと仮定します。

リスト6 採取対象関数showname
static void
showname(const char* name)
{
    .....
}

先ほどの例ですでに、"%s" フォーマットとprobefunc組み込み変数を用いたprintf() による文字列表示を行いましたので、同じ要領でshowname()関数呼び出しにおける文字列引数を表示してみましょう。

リスト7 文字列引数採取Dスクリプト~その1
pid$target:show_args:showname:entry
{
    printf("(%s)\n", arg0);
}

しかし、このDスクリプトでdtraceコマンドを実行してみると……

図2 文字列引数の採取~その1
$ dtrace -s watch_arg_val.d \
         -q \
         -c './show_args'
dtrace: failed to compile script watch_args_str_bad.d: line 3: \
    printf( ) argument #2 is incompatible with conversion #1 prototype:
        conversion: %s
         prototype: char [] or string (or use stringof)
          argument: int64_t
$

何やらエラーが表示されてしまいました。

まず第1の問題は、関数引数に関する型情報が無いことから、各引数がデフォルトでは整数型(この実行例では int64_tとして扱われるため、フォーマット"%s"が期待する型と一致していない点にあります。

もうひとつの問題は、単に文字列領域のアドレスを指定しただけでは、文字列の「安全性」を担保することができない点にあります。

実はDTraceは、指定されたDスクリプトをカーネル空間で実行しています。そのため、普通のアプリケーションのように「変な動作をしてしまったら、いったん止めて再実行」というわけにはいきません。実行時の安全性が確保できない仕組みでは、稼働中のシステムに対しては怖くて適用できないのです。

「文字列」のようなデータ形式は:

  • 妥当な長さであるか不明
  • 本当に "\0" で終端しているか不明
  • 当該メモリ領域が使用可能であるか不明

といった点で「安全」とは言えないため、"char*"のようなアドレス値からprintf("%s") による出力が直接は実施できないように、DTraceがガードをかけているのです。

先の例で使用した probefuncprintf("%s")で表示できたのは、組み込み変数である probefunc の値が「安全な文字列」であることを保証されているためです。

stringof サブルーチン(※3)は、参照先領域に格納されている文字列に対して安全性の確認を行い、⁠安全な文字列」を意味するstring型オブジェクトを返却します。

リスト8 文字列引数採取Dスクリプト~その2
pid$target:show_args:showname:entry
{
    printf("(%s)\n", stringof(arg0));
}

それではエラーメッセージの指示に従い、stringofを使ったDスクリプトを実行してみると……

図3 文字列引数の採取~その2
$ dtrace -s watch_arg_val.d \
         -q \
         -c './show_args'
dtrace: error on enabled probe ID 1 \
    (ID 60308: pid11310:show_args:showname:entry): \
    invalid address (0x8047d40) in action #1
$

またもやエラーが出てしまいました(色違い部分は環境等に応じて変化しますので、違う値でも気にしないでください⁠⁠。

実はこのエラーも、DTrace の実際の処理がカーネル空間で実行されていることに関係があります。

表示しようとしている文字列の格納先arg0の値)は、ユーザプロセス中のアドレス(いわゆるユーザ空間のアドレス)ですから、そのアドレス値を元に処理を実施しようとしても、カーネル空間における当該アドレス値はそもそも無効か、あるいは全然関係の無いデータが格納されている領域かのどちらかです。そのため、上記のようなエラー("invalid address")となってしまうわけです。

文字列引数の表示

前述したように、ユーザプロセスの文字列引数の内容を表示することはできないのでしょうか?

安心してください。以下のようなDスクリプトにより、文字列引数の内容を表示することができます。

リスト 9 文字列引数採取Dスクリプト~その3
pid$target:show_args:showname:entry
{
    printf("show_args(%s)", copyinstr(arg0));
}

copyinstr サブルーチンは、以下のように振る舞います。

  1. 文字列データを、ユーザ空間からカーネル空間に複製(有限長)
  2. string(安全な文字列)型オブジェクトに変換

上記のDスクリプトを実行してみると……

図4 文字列引数の採取~その3
$ dtrace -s watch_arg_val.d \
         -q \
         -c './show_args'
showname(./show_args) ← 採取結果
$

今度は無事に文字列引数の内容を採取することができました。

もしも、
(1) 固定長領域に格納されていて、
(2) 必ずしも "\0" 終端していない文字列を扱う場合は、
以下の方法で文字列表示が可能です。

リスト10 固定長文字列引数採取
pid$target:show_args:showname:entry
{
    printf("show_args(%s)", stringof(copyin(arg0), 64));
}

上記のDスクリプトでは、copyinによりユーザ空間からカーネル空間に64バイト分のデータが転送され、stringofにより当該データが「安全な文字列」であるstring型へと変換されます(必要であれば'\0'の付与も行われます⁠⁠。

メモリ内容の採取

先の採取例では、ポインタ引数が参照する文字列を表示しましたが、実際のプログラムでは、文字列以外の入出力データ=メモリ内容を確認したい、というニーズもあることでしょう。

そこで、データ領域を参照する引数を持つ以下の様な関数checksumを想定します。なお、この関数の実装自体には特に意味はありません。データ領域へのポインタを引数に持っている、という点だけが重要です。

リスト11 採取対象関数checksum
int
checksum(const char* buf)
{
    int val = 0;
    int i;
    int length = 32;
    for(int i = 0 ; i < length ; i += 1){
        val = (val << 1) ^ buf[i];
    }
    return val;
}

先述したように、DTraceはカーネル空間で動作していますので、引数bufが指しているユーザ空間のメモリ内容を一旦カーネル空間にコピーしてから、その内容を表示する必要があります。

そこで、以下のようなDスクリプトを使用します。

リスト12 データ領域内容採取Dスクリプト
pid$target:show_args:checksum:entry
{
    this->iobuf = alloca(32);
    copyinto(arg0, 32, this->iobuf);
    tracemem(this->iobuf, 32);
}

このDスクリプトによって、bufの指す領域は以下のような形式で表示されます。

図5 データ領域内容の採取
$ dtrace -s watch_arg_mem.d \
         -q \
         -c './show_args'
             0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f  0123456789abcdef
         0: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00  .ELF............
        10: 02 00 03 00 01 00 00 00 80 09 05 08 34 00 00 00  ............4...
$

上記の D スクリプトにおけるアクション部分は、以下の手順で処理を行います。

  1. 変数 iobuf を確保
  2. 32 バイトのバッファ領域をalloca サブルーチンによって)カーネル空間に確保
  3. iobuf 変数にバッファ領域を参照させる
  4. checksum関数の引数bufの指すメモリ領域(ユーザ空間)から、iobufの指す領域(カーネル空間)へ32バイト分をcopyintoサブルーチンによって)複製
  5. iobufの指す領域32バイト分をtracememアクションによって)表示

copyintotracememなどは、名称と用法、機能から、どのようなものかは想像が付くと思います。

それではここで初めて出てきたthisというキーワードは何でしょうか?

Dスクリプトでは、this->VariableNameと記述することで、一般的なプログラミング言語で言うところの局所変数に相当する、節固有変数と呼ばれる記憶領域の作成/参照を行うことができるのです(※4⁠⁠。

上記のDスクリプト(リスト12)では、メモリ内容を表示するための一連の処理の間、カーネル内部に確保された一時バッファ領域への参照を保持するために、iobuf変数を使用していることになります。

なお、tracememに指定するデータ長は、実行時可変値を使用することができませんので、可変長バッファの内容を表示したい場合は、一工夫してやる必要があります(※4⁠⁠。

戻り値の採取

これまでに説明してきた手法を組み合わせれば、引数に関する情報採取は十分行えることでしょう。

後は関数の戻り値を採取できれば、関数呼び出しにおける情報採取で必要な情報は、概ね網羅できることになります。

以下のDスクリプトは、プローブ指定における関数名指定部分を空欄にすることで、コマンドshow_args中の全ての関数に対して、戻り値の表示を行います。

リスト13 関数戻り値採取Dスクリプト
pid$target:show_args::return
{
    printf("%s()=0x%p", probefunc, arg1);
}

pidプロバイダでentryプローブを指定するDスクリプトでは、arg0arg1は関数引数の参照に使用しました。しかし、returnプローブを指定するDスクリプトでは、これらの値は違う意味を持ちます。

  • arg0: 関数の戻り先アドレス
  • arg1: 関数の戻り値

何らかのデータが格納されている領域を指すアドレスが戻り値になっていて、その参照先の内容を表示したい場合には、文字列引数や引数参照先メモリ内容の表示と同様に、copyinstrcopyinto + tracememなどを使用する必要があります。

なお、関数の戻り値を保持するarg1の値は、以下の場合には不定となりますので注意してください。

  • 戻り値を持たないvoidな)関数
  • 構造体/オブジェクトを返却する関数

次回予告

関数フローにおける基本的な情報採取について、2回に渡って説明してきましたが、いかがでしたでしょうか?

次回は、マルチスレッドや子プロセス、共有ライブラリなどが関与する状況での注意点について説明します。

おすすめ記事

記事・ニュース一覧