Systemtapプローブ(二)-probeによって生成されたCコード


前の記事では、systemtapのワークフローと、第1、第2段階の内容を簡単に紹介しました.この文章から、Cコードの生成の第3段階に入る.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_exitprobe beginprobe oneshotという関数で呼び出され(対応するsystemtap_module_initstruct stap_be_probeはいずれもtype)、0probe endという関数で呼び出されます.(systemtap_module_exittypeです).名前の通り、1systemtap_module_initはそれぞれセッションの開始と終了時に呼び出されます.systemtapソースコードのsystemtap_module_exitというファイルに呼び出される具体的な流れが表示されます.
システムtapの実行時にはbeginとendフェーズがあり,runtime/transport/transport.txtprobe beginはいずれもbeginフェーズで実行されると考えられる.後者はprobe oneshot関数を呼び出し、endフェーズに入るとマークされます.最後の_stp_exitはendフェーズで実行されます.
では、beginとendの間には、中間段階があるのでしょうか.答えはもちろん肯定的だ.次に、timerを含む例を見てみましょう.
timer probe endprobe 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_newstapiu_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実装の詳細について議論します.