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

第1回 関数フローの採取

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

実行中プロセスの関数フロー採取

先のDスクリプトの説明を見て

$targetの代わりに直接プロセスIDを指定すれば,実行中プロセスの関数フローも採取できるのでは?

と思われた方もいることでしょう。

ご想像の通り,明示的にプロセスIDを指定することで,実行中プロセスの関数フローも採取することができます。以下のDスクリプトを見てください。

リスト5 関数フロー採取スクリプトwatch_flow_attach.d

pid$1:$2::entry,
pid$1:$2::return
{
}

先のDスクリプトとは以下のような違いこそありますが,基本的には同等のものです(※5⁠⁠。

  • プロセスIDの指定に$targetではなく,第1引数と置換される$1を使用
  • トレース採取対象とするバイナリファイル名に,第1引数と置換される$1ではなく,第2引数と置換される$2を使用

それでは,dtraceコマンドを起動してみましょう。

採取対象としては,HTTPD といった既に稼働中のサーバ(※6)でも良いですし,エディタやbashのような対話的なプログラムなどでも(実行時間調整の都合上)良いでしょう。

dtraceコマンド実行における重要な情報は,①採取対象プロセスのプロセスIDと,②そのプロセスのバイナリファイル名の2点です。以下の実行例中のPIDおよびFILENAME部分は,採取対象に応じて適宜置き換えてください。

図2 実行中プロセスからの関数フローの採取

$ dtrace -s ./watch_flow_attach.d \
          -F \
          PID \
          FILENAME

dtraceコマンド経由でlsコマンドを起動した先の例と異なり,採取対象の出力と採取結果が紛れてしまうことが無いですので,今回は-oオプションを省略してみました。

採取対象で何らかの処理が行われるつど,関数フローが出力されているでしょうか?

なお,実行中プロセスのトレースを行うケースでは,例示したDスクリプトを使用する場合,対象プロセスが終了しても dtrace コマンドそのものは終了しません。適宜Ctrl-C等で中断してください。

追記

dtrace コマンド起動の際に,-c オプションによるコマンド指定の代わりに,-p オプションによるプロセスID指定を行えば,リスト1のwatch_flow_whole.dスクリプトをそのまま使用することができますし,対象プロセス終了時にdtraceコマンドも終了します。

ただし,dbx等のデバッガでattach中のプロセスに対しては,この方法は使用できませんので注意してください。

※5)
Dスクリプトは使い捨て,と割り切るのであれば,$1$2のような置換指定を使わずに,直接プロセス ID やバイナリファイル名を記述するのでも構いません。
※6)
サーバの中には,実際の処理の大半をカーネル空間で行っているものもあります(例: Solaris NFSサーバ⁠⁠。それらを採取対象にした場合には,一向に関数フローが採取されない,という可能性もあります。

関数フローの不整合

大変手軽に採取できる関数フローですが,常に完全な関数フローが採取できるわけではありません。

たとえば,以下のようなCプログラムを採取対象にしたとします。

リスト6 最適化確認用プログラム

static void f1(){ /* nop */ }

static void f2(){ f1(); }

static void f3(){ f1(); f2(); }

int
main(int argc,
     const char* argv[])
{
    f3();
    return 0;
}

特にオプション指定をしないコンパイル結果のバイナリファイルからは,以下のような関数フローが採取できます。

リスト7 最適化無しの場合の関数フロー

  0    -> main
  0      -> f3
  0        -> f1
  0        <- f1
  0        -> f2
  0          -> f1
  0          <- f1
  0        <- f2
  0      <- f3
  0    <- main

見ての通り,この関数フローはソースコードと対応が取れています。

しかし,最適化オプション-O3を指定して生成したバイナリファイルや,さらに最適化度の高い-O4を指定して生成したバイナリファイルからは,リスト8やリスト9のようにソースファイルから想定するものとは異なる関数フローが採取されます(※7⁠⁠。

リスト8 -O3最適化時の関数フロー

  0    -> main
  0      -> f3
  0        -> f1
  0        <- f1
  0      <- f3
  0      -> f2
  0      <- f2
  0      -> f1
  0      <- f1
  0    <- main

リスト9 -O4最適化時の関数フロー

  0    -> main
  0    <- main

関数フロー出力において,字下げが綺麗に表示されなかったり,呼び出しと復帰の対応が取れていないといった不整合は,概ねこのような最適化による影響です。

コンパイラの最適化技法の話に踏み込むことになりますので,この場での詳細説明は割愛しますが,最適化が施されたバイナリファイルから採取された関数フローは,必ずしもソースファイルにおける記述とは対応しない場合がある,ということは覚えておいてください。

※7)
採取結果の貼り付けに失敗したわけではありません。⁠関数呼び出しをインライン展開」する最適化が施されたことで,関数フローが採取されなかったのです。

次回予告

まずは手始めに実行時関数フローを採取してみましたが,いかがでしたでしょうか?

次回は,単に関数の呼び出しフローを追いかけるだけではなく,さらに詳細な情報の採取方法について説明をする予定です。

著者プロフィール

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

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