前回はDTraceを用いて関数呼び出しフローを採取しました。今回は、関数呼び出しにおける、より詳細な情報の採取を行います。
引数値の採取
関数呼び出しにおいて採取したい情報の筆頭は、何と言っても引数に関する情報でしょう。
引数値の表示
まずは、以下の様な関数 multiply()
を持つプログラムshow_args を想定します。
この関数が呼び出された際の引数を表示するDスクリプトは、以下のように記述します(自前のプログラムに対して試す場合は、show_args
およびmultiply
部分を適宜書き換えてください)。
上記Dスクリプトの各要素は、以下の様な意味を持ちます("pid$target:show_args
" および"entry
"部分に関しては、前回で解説済みですので割愛します)。
表1 Dスクリプトの構成要素
multiply |
この位置には情報採取対象の関数名を記述します。
この例では、multiply 関数が採取対象となります。
|
printf() |
C 標準ライブラリの printf()
関数に相当する処理を行う DTraceアクションを呼び出します。
標準Cライブラリで既定されているフォーマット指定は概ね使用可能です。
|
arg0 , arg1 |
(この例では)関数呼び出しにおける引数を参照するのに使用します。
最初の引数が arg0 、以降の引数が順に arg1 、arg2 ...で参照可能です。
範囲外の引数を参照した場合の値は不定となります。
|
dtrace
コマンドの起動方法は前回と同様です。
今回は関数フローの必要が無いので、-F
指定を省略しています。
また -q
指定により、DTraceによる関数名やCPU ID等の付加情報表示を抑止して、D スクリプトの(printf()
アクションによる)明示的な出力指定のみを表示するようにしています。
D スクリプトの文法詳細
これまでは、個々の状況におけるDスクリプトの説明をしてきましたが、ここで改めてDスクリプトの文法について説明したいと思います。
D スクリプトは、以下の構造を1つ、ないし複数記述したものです(※1)。
<Action>
部分は、先の例で記述した printf()
のような処理を、セミコロン区切りで複数列挙することができます。
<Probe-Description>
は以下の形式で構成されます。
各要素は以下の意味を持ちます。
表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スクリプト中からそれぞれprobeprov
、probemod
、probefunc
および probename
という組み込み変数を使って参照することができます。
そのため、(1) <Probe-Description>
はカンマで区切って複数列挙できることと、(2) 現時点で <Action>
の対象となっている関数名をprobefunc
参照できることを利用して:
上記のように、1つの<Action>
を、複数の関数に対する<Probe-Description>
で共有しつつ、実際に呼び出された関数の名前を表示内容に含めることが可能です。
上記以外の組み込み変数に関しては、Solaris動的トレースガイドの「組み込み変数」の解説を参照してください。
引数文字列の採取
文字列表示に関する制限
関数呼び出しにおける引数値表示の次は、文字列引数の内容を表示してみましょう。
show_args
コマンドは、main()
関数のargv[0]
引数、つまり自身の名前を使って以下の関数showname
を呼び出すものと仮定します。
先ほどの例ですでに、"%s
" フォーマットとprobefunc
組み込み変数を用いたprintf()
による文字列表示を行いましたので、同じ要領でshowname()
関数呼び出しにおける文字列引数を表示してみましょう。
しかし、このDスクリプトでdtrace
コマンドを実行してみると……
何やらエラーが表示されてしまいました。
まず第1の問題は、関数引数に関する型情報が無いことから、各引数がデフォルトでは整数型(この実行例では int64_t
)として扱われるため、フォーマット"%s
"が期待する型と一致していない点にあります。
もうひとつの問題は、単に文字列領域のアドレスを指定しただけでは、文字列の「安全性」を担保することができない点にあります。
実はDTraceは、指定されたDスクリプトをカーネル空間で実行しています。そのため、普通のアプリケーションのように「変な動作をしてしまったら、いったん止めて再実行」というわけにはいきません。実行時の安全性が確保できない仕組みでは、稼働中のシステムに対しては怖くて適用できないのです。
「文字列」のようなデータ形式は:
- 妥当な長さであるか不明
- 本当に "\0" で終端しているか不明
- 当該メモリ領域が使用可能であるか不明
といった点で「安全」とは言えないため、"char*"のようなアドレス値からprintf("%s")
による出力が直接は実施できないように、DTraceがガードをかけているのです。
先の例で使用した probefunc
がprintf("%s")
で表示できたのは、組み込み変数である probefunc
の値が「安全な文字列」であることを保証されているためです。
stringof
サブルーチン(※3)は、参照先領域に格納されている文字列に対して安全性の確認を行い、「安全な文字列」を意味するstring
型オブジェクトを返却します。
それではエラーメッセージの指示に従い、stringof
を使ったDスクリプトを実行してみると……
またもやエラーが出てしまいました(色違い部分は環境等に応じて変化しますので、違う値でも気にしないでください)。
実はこのエラーも、DTrace の実際の処理がカーネル空間で実行されていることに関係があります。
表示しようとしている文字列の格納先(arg0
の値)は、ユーザプロセス中のアドレス(いわゆるユーザ空間のアドレス)ですから、そのアドレス値を元に処理を実施しようとしても、カーネル空間における当該アドレス値はそもそも無効か、あるいは全然関係の無いデータが格納されている領域かのどちらかです。そのため、上記のようなエラー("invalid address
")となってしまうわけです。
文字列引数の表示
前述したように、ユーザプロセスの文字列引数の内容を表示することはできないのでしょうか?
安心してください。以下のようなDスクリプトにより、文字列引数の内容を表示することができます。
copyinstr
サブルーチンは、以下のように振る舞います。
- 文字列データを、ユーザ空間からカーネル空間に複製(有限長)
string
(安全な文字列)型オブジェクトに変換
上記のDスクリプトを実行してみると……
今度は無事に文字列引数の内容を採取することができました。
もしも、
(1) 固定長領域に格納されていて、
(2) 必ずしも "\0" 終端していない文字列を扱う場合は、
以下の方法で文字列表示が可能です。
上記のDスクリプトでは、copyin
によりユーザ空間からカーネル空間に64バイト分のデータが転送され、stringof
により当該データが「安全な文字列」であるstring
型へと変換されます(必要であれば'\0'の付与も行われます)。
メモリ内容の採取
先の採取例では、ポインタ引数が参照する文字列を表示しましたが、実際のプログラムでは、文字列以外の入出力データ=メモリ内容を確認したい、というニーズもあることでしょう。
そこで、データ領域を参照する引数を持つ以下の様な関数checksum
を想定します。なお、この関数の実装自体には特に意味はありません。データ領域へのポインタを引数に持っている、という点だけが重要です。
先述したように、DTraceはカーネル空間で動作していますので、引数buf
が指しているユーザ空間のメモリ内容を一旦カーネル空間にコピーしてから、その内容を表示する必要があります。
そこで、以下のようなDスクリプトを使用します。
このDスクリプトによって、buf
の指す領域は以下のような形式で表示されます。
上記の D スクリプトにおけるアクション部分は、以下の手順で処理を行います。
- 変数
iobuf
を確保
- 32 バイトのバッファ領域を(
alloca
サブルーチンによって)カーネル空間に確保
iobuf
変数にバッファ領域を参照させる
checksum
関数の引数buf
の指すメモリ領域(ユーザ空間)から、iobuf
の指す領域(カーネル空間)へ32バイト分を(copyinto
サブルーチンによって)複製
iobuf
の指す領域32バイト分を(tracemem
アクションによって)表示
copyinto
やtracemem
などは、名称と用法、機能から、どのようなものかは想像が付くと思います。
それではここで初めて出てきたthis
というキーワードは何でしょうか?
Dスクリプトでは、this->VariableName
と記述することで、一般的なプログラミング言語で言うところの局所変数に相当する、節固有変数と呼ばれる記憶領域の作成/参照を行うことができるのです(※4)。
上記のDスクリプト(リスト12)では、メモリ内容を表示するための一連の処理の間、カーネル内部に確保された一時バッファ領域への参照を保持するために、iobuf
変数を使用していることになります。
なお、tracemem
に指定するデータ長は、実行時可変値を使用することができませんので、可変長バッファの内容を表示したい場合は、一工夫してやる必要があります(※4)。
戻り値の採取
これまでに説明してきた手法を組み合わせれば、引数に関する情報採取は十分行えることでしょう。
後は関数の戻り値を採取できれば、関数呼び出しにおける情報採取で必要な情報は、概ね網羅できることになります。
以下のDスクリプトは、プローブ指定における関数名指定部分を空欄にすることで、コマンドshow_args
中の全ての関数に対して、戻り値の表示を行います。
pid
プロバイダでentry
プローブを指定するDスクリプトでは、arg0
やarg1
は関数引数の参照に使用しました。しかし、return
プローブを指定するDスクリプトでは、これらの値は違う意味を持ちます。
arg0
: 関数の戻り先アドレス
arg1
: 関数の戻り値
何らかのデータが格納されている領域を指すアドレスが戻り値になっていて、その参照先の内容を表示したい場合には、文字列引数や引数参照先メモリ内容の表示と同様に、copyinstr
やcopyinto
+ tracemem
などを使用する必要があります。
なお、関数の戻り値を保持するarg1
の値は、以下の場合には不定となりますので注意してください。
- 戻り値を持たない(
void
な)関数
- 構造体/オブジェクトを返却する関数
次回予告
関数フローにおける基本的な情報採取について、2回に渡って説明してきましたが、いかがでしたでしょうか?
次回は、マルチスレッドや子プロセス、共有ライブラリなどが関与する状況での注意点について説明します。