Systemtapプローブ(二)-probeによって生成されたCコード
6622 ワード
前の記事では、systemtapのワークフローと、第1、第2段階の内容を簡単に紹介しました.この文章から、Cコードの生成の第3段階に入る.
hello, world
慣例に従って、まず「hello world」の例から始めます.
私の趣味から、ここでは完全なhello worldを3つに切った.特定の文字列を検索することで,生成されたCコードからこの3つのprobeが生成に対応するコードを迅速に見つけることができる.
各probeは実行時にcontextパラメータを渡すことがわかります.各contextパラメータには
次に、
次は
残りの2つの
各probeには、対応する
この行の前に準備コードがあり、その後、実行中にエラーが発生したかどうかや実行時間を統計したかどうかを確認します.注意probe関数に渡される
システムtapの実行時にはbeginとendフェーズがあり,
では、beginとendの間には、中間段階があるのでしょうか.答えはもちろん肯定的だ.次に、timerを含む例を見てみましょう.
timer
生成されたprobeに対応するCコードを比較すると,基本的には元と同じである.しかしprobe部分以外には2つの違いがあります.
一つは
二つ目は
これは、systemtapの実行に時間がかかりすぎることを回避するために設定され、カーネルが応答を失うことを防止するためである.
timer付きstpスクリプトで生成されたCコードでは,beginフェーズの後に
次に、uprobe付きの例を見てみましょう.
uprobe
上のstpコードにはluajit実行可能ファイルの
生成されたCコードのうち,このprobeに対応するタイプは
不思議なことに、この中の
一致するプロセスごとにprobeが実行されることを強調します.
uretprobe
最後に、uprobeと対向するuretprobeの場合を見てみましょう.
上記のstpコードから生成されるCコードは、基本的にuprobeに類似している.ただ
予告
次はstpの様々なタイプが対応するCコードにどのようにコンパイルされているかを見て、より多くのsystemtap実装の詳細について議論します.
stap -v test.stp -p3 > out.c
というコマンドで、stapに生成されたCコードをout.c
にリダイレクトさせることができます.hello, world
慣例に従って、まず「hello world」の例から始めます.
probe begin {
printf("hello")
}
probe oneshot {
printf(" wor")
}
probe end {
printf("ld
")
}
私の趣味から、ここでは完全なhello worldを3つに切った.特定の文字列を検索することで,生成されたCコードからこの3つのprobeが生成に対応するコードを迅速に見つけることができる.
static void probe_3646 (struct context * __restrict__ c) {
__label__ deref_fault;
__label__ out;
struct probe_3646_locals * __restrict__ l = & c->probe_locals.probe_3646;
(void) l;
if (c->actionremaining < 1) { c->last_error = "MAXACTION exceeded"; goto out; }
(void)
({
_stp_print ("hello");
});
deref_fault: __attribute__((unused));
out:
_stp_print_flush();
}
probe begin
に対応するコードです.各probeは実行時にcontextパラメータを渡すことがわかります.各contextパラメータには
struct probe_id_locals
変数があります.この変数はローカル変数を格納するために使用されます.もちろん、hello worldの例ではローカル変数は使用されていないので、空です.次に、
MAXACTION exceeded
の部分をチェックします.この部分はsystemtapのドキュメントを参照して、1つのsystemtap probeの実行時間を制限し、カーネルが応答を失うことを避ける状況です.次は
(void)
({
_stp_print ("hello");
});
printf
という文が対応する組み込み関数の呼び出しにコンパイルされていることがわかります.また、汚染を防ぐために、各文のコンパイル結果にはわざわざカッコとカッコを付けた.残りの2つの
probe
は大同小異で、probe oneshot
が1つ増えるだけです.function___global_exit__overload_0
は、function___global_exit__overload_0
内蔵関数を呼び出します.各probeには、対応する
_stp_exit
インスタンスがあります.コードから分かるように、struct stap_be_probe
関数はこのprobeのhandlerを実行します.具体的には、このような行です. (*stp->probe->ph) (c);
この行の前に準備コードがあり、その後、実行中にエラーが発生したかどうかや実行時間を統計したかどうかを確認します.注意probe関数に渡される
enter_be_probe
は多重化される.context
は、enter_be_probe
およびsystemtap_module_init
によって呼び出される.具体的には、systemtap_module_exit
とprobe begin
はprobe oneshot
という関数で呼び出され(対応するsystemtap_module_init
のstruct stap_be_probe
はいずれもtype
)、0
はprobe end
という関数で呼び出されます.(systemtap_module_exit
はtype
です).名前の通り、1
とsystemtap_module_init
はそれぞれセッションの開始と終了時に呼び出されます.systemtapソースコードのsystemtap_module_exit
というファイルに呼び出される具体的な流れが表示されます.システムtapの実行時にはbeginとendフェーズがあり,
runtime/transport/transport.txt
とprobe begin
はいずれもbeginフェーズで実行されると考えられる.後者はprobe oneshot
関数を呼び出し、endフェーズに入るとマークされます.最後の_stp_exit
はendフェーズで実行されます.では、beginとendの間には、中間段階があるのでしょうか.答えはもちろん肯定的だ.次に、timerを含む例を見てみましょう.
timer
probe end
をprobe oneshot
に変更します.probe timer.ms(149) {
printf(" wor")
exit()
}
生成されたprobeに対応するCコードを比較すると,基本的には元と同じである.しかしprobe部分以外には2つの違いがあります.
一つは
probe timer.ms(149)
に対応していないprobe timer.ms(149)
です.struct stap_be_probe
はbeginまたはendフェーズで実行されないためです.二つ目は
probe timer.ms(149)
種類が増えたことです.これがstruct stap_hrtimer_probe
対応のprobeタイプです.生成されたコードから、probe timer.ms(149)
の中にsystemtap_module_init
があることがわかります.この関数は_stp_hrtimer_create
に登録されています._stp_hrtimer_notify_function
はほぼ_stp_hrtimer_notify_function
の翻版である.enter_be_probe
は、実行時間を統計するときに1つのチェックを追加しました. if (interval > STP_OVERLOAD_INTERVAL) {
if (c->cycles_sum > STP_OVERLOAD_THRESHOLD) {
_stp_error ("probe overhead exceeded threshold");
atomic_set (session_state(), STAP_SESSION_ERROR);
atomic_inc (error_count());
}
c->cycles_base = cycles_atend;
c->cycles_sum = 0;
}
これは、systemtapの実行に時間がかかりすぎることを回避するために設定され、カーネルが応答を失うことを防止するためである.
timer付きstpスクリプトで生成されたCコードでは,beginフェーズの後に
_stp_hrtimer_notify_function
を介してendフェーズに切り込むのではなく,timerを登録し,timerでprobeの論理を実行する.その後、timerで_stp_exit
が呼び出されたためendフェーズに切り込む.次に、uprobe付きの例を見てみましょう.
uprobe
probe process("/usr/local/openresty/luajit/bin/luajit").function("lj_str_new") {
printf(" wor")
exit()
}
上のstpコードにはluajit実行可能ファイルの
_stp_exit
関数がマウントされています.注意このスクリプトを実行するには、luajitのdebuginfoが提供されていることを確認する必要があります.生成されたCコードのうち,このprobeに対応するタイプは
lj_str_new
である.static struct stapiu_consumer stap_inode_uprobe_consumers[] = {
{ .target=&stap_inode_uprobe_targets[0], .offset=(loff_t)0x6a55ULL, .probe=(&stap_probes[1]), },
};
不思議なことに、この中の
stapiu_consumer
です.コードにはこの数はありませんが、どうやって来たのでしょうか.0x6a55
によって、この関数のアドレスは0 x 406 a 55であることがわかります.もちろん、実際の実行アドレスはX+0 x 406 a 55であり、Xはランダムであるべきである.0 x 40000はプログラムリンク時に固定されたベースアドレスであるため,readelf -s /usr/local/openresty/luajit/bin/luajit | grep lj_str_new
のアドレスはX+0 x 40000+0 x 6 a 55であると考えられる.すなわち、0 x 6 a 55をoffsetとしてlj_str_new
という関数の位置を決定することができる.これもluajitのdebuginfoを提供する必要がある理由です.debuginfoがなければ、lj_str_new
のアドレスを特定できないからです.lj_str_new
はstapiu_consumer
で実行され、実行プロセスの前の2つのprobeと同じです.Systemtapは、現在存在し、新しく作成されたすべてのプロセスをチェックします.一部のプロセスの実行可能ファイルがprobeに一致すると、対応するprobeがカーネルAPIを介して登録されます.カーネルがコールバックをトリガーすると、この関数が実行されます.一致するプロセスごとにprobeが実行されることを強調します.
stapiu_probe_handler
を指定すると、実際には-x PID
の値しか設定されません.複数のプロセスにトリガーされたくない場合は、stpコードで自分で解決する必要があります.probe process("/usr/local/openresty/luajit/bin/luajit").function("lj_str_new") {
_target = target();
if (pid() != _target) {
next;
}
printf(" wor")
exit()
}
target()
も同様であり、このオプションは実際にはサブプロセスを作成し、そのサブプロセスのPIDを-c CMD
の値とする.uretprobe
最後に、uprobeと対向するuretprobeの場合を見てみましょう.
probe process("/usr/local/openresty/luajit/bin/luajit").function("lj_str_new").return {
printf(" wor")
exit()
}
上記のstpコードから生成されるCコードは、基本的にuprobeに類似している.ただ
target()
は少し違います.static struct stapiu_consumer stap_inode_uprobe_consumers[] = {
{ .return_p=1, .target=&stap_inode_uprobe_targets[0], .offset=(loff_t)0x6a55ULL, .probe=(&stap_probes[1]), },
};
stapiu_consumer
が増えました.予告
次はstpの様々なタイプが対応するCコードにどのようにコンパイルされているかを見て、より多くのsystemtap実装の詳細について議論します.