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

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

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

前回は,独自に定義したプロバイダのプローブをプログラム中に埋め込むことで,関数呼び出しの境界以外における情報採取の方法を説明しました。

今回は,プローブに指定された値を元に,より詳細な情報を採取するための手法について説明します。

なお,本稿で説明する手法は,必ずしも独自プロバイダを使用する場合に限定したものではありませんので,pidプロバイダを使用する場合にも利用可能です。

可変長領域ダンプの採取

まずは,第2回では固定長で行っていたメモリ領域の内容表示を,可変長領域に対応させる方法について説明します。

採取対象の準備

まずはリスト1に示す独自プロバイダuserioを定義するものと仮定します。

リスト1 プロバイダ定義userio.d)

provider userio
{
    probe readin(void* buf, size_t len);
    probe writeout(void* buf, size_t len);
};

userioプロバイダが提供するプローブは,ユーザプログラムにおける何らかのI/O実施の際に,readinプローブなら実際のI/O実施直後に,writeoutプローブなら実際のI/O実施に先立って使用することで,内容の記録や確認を行うような用途を想定しています。

プロバイダを定義したなら,以下の手順で実行可能バイナリを生成します。

  1. ヘッダフィルの生成
  2. 採取対象ソースファイルread_n_write.cの作成
  3. 採取対象ソースファイルのコンパイルread_n_write.oの生成)
  4. dtrace コマンドによるリンク前処理userio.oの生成)
  5. 採取対象実行可能バイナリのリンクread_n_writeの生成)

作業手順例を図1に示します。

図1 採取対象実行可能バイナリの生成

$ dtrace -s userio.d -h (1)
※read_n_write.cの作成(2)
$ cc -c read_n_write.c (3)
$ dtrace -s userio.d -G read_n_write.o (4)
$ cc -o read_n_write read_n_write.o  userio.o (5)
$

tracemem()の問題点

第2回でデータ領域内容の採取に使用したDスクリプトをリスト2に再掲します。

リスト2 データ領域内容採取Dスクリプト

pid$target:show_args:checksum:entry
{
    this->iobuf = alloca(32);
    copyinto(arg0, 32, this->iobuf);
    tracemem(this->iobuf, 32);
}

tracemem()は,サイズ指定を行う引数が固定値でなければならないため,可変長領域の内容採取に直接使用することはできません。

また,Dスクリプトはループ制御を持たない仕様ですので,可変長領域のサイズに応じてtracemem()を繰り返す,といった記述をすることもできません。

しかし,可変長領域のダンプを行うにあたって,メガバイト単位の採取にまで対応することは,あまり現実的ではありません。記録に必要なディスク容量や,事後参照の利便性などを考えた場合,せいぜい数キロバイトの記録を行うのが現実的な線ではないでしょうか?

数キロバイトまでの対応で割り切ることができるのであれば,tracemem()による固定長のダンプを,可変長領域のサイズに応じて,必要なだけ繰り返し実行する手段を講じればよいことになります。

可変長領域ダンプの実現

さて,それではループ制御を持たないDスクリプトにおいて,⁠可変長領域のサイズに応じて,必要なだけ繰り返し実行」するには,どうすればよいでしょうか?

ここでは,Dスクリプトの以下の性質を利用します。

  • スクリプト中の各節は,記述順序に実行される
  • 述語(=前提条件)記述には,改変を伴う式を記述することができる

繰り返し処理の大枠の構造を,リスト3に示します。

リスト3 擬似的な繰り返し処理

inline uintptr_t width = 128;

/* 節 (1) */
userio$target:$1::
{ self->offset = - width; }

/* 節 (2) */
userio$target:$1::
/(self->offset += width) < arg1/
{ ※ self->offset は 0 }

/* 節 (3) */
userio$target:$1::
/(self->offset += width) < arg1/
{ ※ self->offset は 128 }

/* 節 (4) */
userio$target:$1::
/(self->offset += width) < arg1/
{ ※ self->offset は 256 }
        :
    (以下,同じ記述の繰り返し)
        :

冒頭の"inline int width = 128;"は,Dスクリプト内で定数値をシンボル化するための宣言です。このような定数シンボルを使用することで,事後の保守における改変の利便性が向上します。

第1節で"- width",即ち-128が設定された"self->offset" 変数は,第2節以降の述語部分での"self->offset += width"記述により,各節では順次0,128,256,....と評価されます。

つまり,必要とされる採取量が高々数キロバイト程度であるとの仮定があるなら,必要とされる上限サイズに達するまで第2節と同じ記述を手動で列挙することで,擬似的な繰り返し処理が実現されるわけです。

この方法による擬似的な繰り返しが,一般的なプログラミング言語における繰り返しと違うのは,"self->offset"変数の値が有効値(この例ではarg1)を越えた後も,残りの節の実行(但し述語成立の判定のみ)が継続される点です。

なお,第1節で "self->offset" の値をわざわざ負の値に初期化しているのは,"self->offset"が0の場合の処理を特別扱いしないことで,第2節以降の記述を完全にコピー&ペーストで済ませることができるためです。

実際に可変長領域をダンプするDスクリプトをリスト4に示します。

リスト 4 可変長データ領域内容採取 D スクリプト

inline uintptr_t width = 128;

userio$target:$1::
{
    printf("addr=0x%p", arg1);
    self->offset = - width;
}
userio$target:$1::
/(self->offset += width) < arg1/
{
    self->buf = alloca(width);
    copyinto(arg0 + self->offset, width, self->buf);
    printf("+0x%p -", self->offset);
    tracemem(self->buf, width);
}
        :
    (以下,同じ記述の繰り返し)
        :

このスクリプトによる実行例を図 2 に示します。

図2 可変長データ領域内容の採取

$ dtrace -s watch_io.d \
         -c './read_n_write ofile 400 300' \
         read_n_write \
  < ./read_n_write
dtrace: script 'watch_io.d' matched 10 probes
CPU     ID                    FUNCTION:NAME
  0  60346                      main:readin addr=0x8062a10
  0  60346                      main:readin +0x0 -
             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 cc 10 05 08 34 00 00 00  ............4...
        20: 48 38 00 00 00 00 00 00 34 00 20 00 06 00 28 00  H8......4. ...(.
        30: 26 00 25 00 06 00 00 00 34 00 00 00 34 00 05 08  &.%.....4...4...
        40: 00 00 00 00 c0 00 00 00 c0 00 00 00 05 00 00 00  ................
        50: 00 00 00 00 03 00 00 00 f4 00 00 00 00 00 00 00  ................
        60: 00 00 00 00 11 00 00 00 00 00 00 00 04 00 00 00  ................
        70: 00 00 00 00 fd ff ff 6f 08 01 00 00 08 01 05 08  .......o........

  0  60346                      main:readin +0x80 -
             0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f  0123456789abcdef
         0: 00 00 00 00 10 00 00 00 00 00 00 00 04 00 00 00  ................
        10: 00 00 00 00 01 00 00 00 00 00 00 00 00 00 05 08  ................
        20: 00 00 00 00 a9 23 00 00 a9 23 00 00 05 00 00 00  .....#...#......
        30: 00 00 01 00 01 00 00 00 ac 23 00 00 ac 23 06 08  .........#...#..
        40: 00 00 00 00 6c 02 00 00 58 06 00 00 07 00 00 00  ....l...X.......
        50: 00 00 01 00 02 00 00 00 40 24 00 00 40 24 06 08  ........@$..@$..
        60: 00 00 00 00 58 01 00 00 00 00 00 00 07 00 00 00  ....X...........
        70: 00 00 00 00 2f 75 73 72 2f 6c 69 62 2f 6c 64 2e  ..../usr/lib/ld.
....(以下略)

この例では,実行時に指定された400というデータ長を用いて読み込んだデータの内容を,readinプローブを契機に,128バイトごとに区切って出力しています。

なお,リスト4では視認性の点から簡素化していますが,本来copyinto()サブルーチンに指定するサイズ情報は,リスト5のように記述すべきです。

リスト5 サイズ指定超過の防止

copyinto(arg0 + self->offset, 
         (arg1 - self->offset < width
          ? arg1 - self->offset
          : width), 
         self->buf);

この記述により,有効データの残量がwidthよりも小さい場合には,有効なデータの分だけがcopyinto()アクションによるカーネル空間への取り込み対象となります。

copyinto()アクションによるユーザ空間からカーネル空間へのコピーの際に,サイズ指定が過剰である場合,当該プロセスには割り当てられていないメモリ領域に対してもカーネル空間への取り込み=メモリアクセスが実行されてしまうことで,不正領域アクセス=メモリフォールトが発生し,Dスクリプトが期待通りに実行されない可能性があります。

サイズ指定をリスト5のように記述することで,カーネル空間へのコピー対象を,有効なデータが格納されている範囲に限定することができます。

なお,"a ? b : c"形式の"条件式"(conditional expression)⁠※1)は,述語記述による条件分岐を除けば,Dスクリプト内において使用できる唯一の制御構文ですので,憶えておくと何かと便利です。

※1)
「3項式」と呼ばれる場合もあります

著者プロフィール

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

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

コメント

コメントの記入