Ubuntu Weekly Recipe

第692回sysfsやbpftoolを用いたeBPFの活用

第688回第690回では、カーネルのトレーシングツールとして注目されているeBPFを活用するためのツールとしてBCCを紹介しました。しかしながら、BCCだけがeBPFを扱えるツールというわけではありません。今回はツールなしに利用できるsysfsや、よりユーザーフレンドリーなトレーシングツールであるbpftoolを紹介します。

Python版BCCの問題点

これまで紹介していたBPF Compiler CollectionBCCのツールはいずれもフロントエンドとしてPythonを使っていました。つまり利用者はまずPythonスクリプトを起動し、その中でeBPFのオブジェクトをコンパイルし、ロードすることでようやくトレースが始まっていたのです。

実行環境でBPFオブジェクトをビルドする必要があるこの方法にはいくつかの問題点が存在します。

  • 実行環境にコンパイラをインストールする必要がある
  • 実行環境にカーネルのヘッダーファイルが必要になる
  • 実行時にコンパイルという重い処理が走る

この問題は日常的に開発に利用しているデスクトップ環境やサーバーならそこまで影響はありません。しかしながらプロダクション用途となると話は別です。セキュティ的にもメンテナンス的にもできる限り余計なものはインストールしたくないでしょう。障害の事前検知手段としてeBPFを使いたいと考えたときに、そのトレースツールの処理の重さが新たな障害のトリガーなってしまっては目も当てられません。サーバーのリソースモニタリングや、ネットワークフレームの処理などにeBPFを使用したいとき、上記のような制約が存在すると導入の障害になるのです[1]⁠。

対応策として、いくつかの方法が考えられます。

  • Clang等でBPFオブジェクトをコンパイルし、別途ロードする
  • sysfsの機能を用いてトレーシングする
  • bpftrace等の別のツールを使う
  • BPF CO-REによりポータブルなバイナリを生成する

Clang等でBPFオブジェクトをコンパイルし、別途ロードする

Clang等を使えばBPFオブジェクトをコンパイルできます。これをさらにカーネルにロードしてしまえば、BCCのバックエンドと同じことを実現できます。しかしながらきちんと動くBPFオブジェクトを作るにはそれなりの知識が必要です。そこが大変だからこそ、Python版のBCCが登場し、広く使われたという経緯が存在します。よって最初の選択肢は、条件次第では採用できるものの、多くのケースにおいては他の選択肢を使ったほうが「楽ができる」でしょう。

sysfsの機能を用いてトレーシングする

2番目の選択肢としてカーネルのsysfsにあるトレース機能を使ってトレーシングを行う方法が考えられます。この方法ならシステムに追加でソフトウェアをインストールしなくても、トレーシングできるというメリットがあります。ただし制約はいくつか存在します。一番大きいのが、トレース結果をtrace_pipeでしか取得できないことでしょう。つまりユーザーランド側はtrace_pipeの出力結果を解析する必要があります。表示するデータの量は変更可能ではあるのですが、あくまでprintfによる出力であるため、渡せるデータの内容やその方法については限定されます。それでも結局のところ、デバッグ目的ならなんらかの出力は必須であるため、簡単に調査する分には問題とはならないでしょう。

bpftrace等の別のツールを使う

より高機能なトレーシングツールとしてbpftraceが存在します。内部的にやっていることはBCCと同じでその場でコンパイルすることになるのですが、カーネルのBTF(BPF Type Format)を活用することでその依存関係を極力減らしています。また独自の言語を採用することで、トレーシングの表現がしやすくなっているのが特徴のひとつです。ちなみに現在はBCCと同じIO Visorプロジェクトで開発が行われています。eBPFを「トレーシング目的」で使うなら、BCCよりもbpftraceを使うほうが簡単かもしれません。独自言語の学習コストはありますが、そこまで難しくはないのですぐに理解できるようになるでしょう。最新版がsnapパッケージとして提供されているため、導入も容易です。

BPF CO-REによりポータブルなバイナリを生成する

最後の方法が現在注目されている、BPF CO-RE(Compile Once – Run Everywhere)を活用する方法です。つまり一度特定の環境でBPFを利用したプログラムバイナリをビルドしておけば、そのバイナリをたとえカーネルのバージョンが異なっていても他の環境で実行できる仕組みです。これにより実行環境にはバイナリだけコピーすれば良いことになります。

BPF CO-REもbpftraceと同じくカーネルのBTFを活用します。BTFは端的に言うとBPFプログラムを動かすために必要な、実行中のカーネルの各種情報を提供する仕組みです。最近のカーネルはこのBTFを大幅に拡張し、BPFプログラムが実行時に自動的にカーネルのバージョン間の差異を自動的に調整できるようになりました。さらにClangはそのBTFを活用したBPFオブジェクトを生成できるようになり、そんなBPFオブジェクトをロードするためのライブラリであるlibbpfは、ロード時にBTFとBPFオブジェクトを適切に結びつけるようになりました。

つまり「BPF CO-RE」とはBPFオブジェクトを再利用可能にするために各種ツールをアップデートした状態をまとめたものであり、BPF CO-REというソフトウェアが存在するというわけではありません。

結局何を使うべきか

このように、BCC以外にも複数の選択肢が存在します。またここでは紹介しなかったツールや、トレーシング以外にeBPFを使うツールも存在します。結局のところ、やりたいことに合わせてツールを変えるのがベストな方法です。

今回例をあげたもの中だと、BPF CO-REは注目株になっています。ただしBPF CO-RE(や最新のBTF)を活用するためには、そこそこ新しいカーネルやツールチェインが必要です。Ubuntuだと最初に動くようになったのが、20.10の頃になります。つまり現時点で最新のLTSであるUbuntu 20.04 LTSでは、BPF CO-REの恩恵は受けられません。また、BPF/BTF自体がまだ活発に機能拡張されているため、本格的に使うならより新しいカーネルやツールチェインを使いたいところです。よってUbuntuの場合は、2022年4月にリリースされる次のLTSであるUbuntu 22.04 LTSぐらいから、プロダクション用途でも使われるようになっていくのではないでしょうか[2]⁠。

今回はsysfsとbpftraceを使う方法を紹介します。BPF CO-REについては次回以降に解説予定です。

今一度execveをトレースする

まずは最初に第690回で解説した、execve()をトレースするPython版BCC向けのコードexecve.pyを再掲しておきましょう。これはexecsnoopをよりシンプルにしたようなコードです。今回はこれを各種ツールを活用した形に作り直すことにします。

#!/usr/bin/python3
from bcc import BPF

bpf_text="""
#include <linux/sched.h>

struct data_t {
    u32 pid;
    u32 ppid;
    char comm[TASK_COMM_LEN];
    char fname[128];
};
BPF_PERF_OUTPUT(events);

int syscall__execve(struct pt_regs *ctx, const char __user *filename)
{
    struct data_t data = {};
    struct task_struct *task;

    data.pid = bpf_get_current_pid_tgid() >> 32;

    task = (struct task_struct *)bpf_get_current_task();
    data.ppid = task->real_parent->tgid;

    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    bpf_probe_read_user(data.fname, sizeof(data.fname), (void *)filename);

    events.perf_submit(ctx, &data, sizeof(struct data_t));
    return 0;
}
"""

b = BPF(text=bpf_text)
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="syscall__execve")

print("PID      PPID     COMM             FNAME")
def print_event(cpu, data, size):
    event = b["events"].event(data)
    print("{:<8} {:<8} {:16} {}".format(event.pid, event.ppid, event.comm.decode(), event.fname.decode()))

b["events"].open_perf_buffer(print_event)
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

BCC(bpfcc)がインストールされている環境で上記を実行すると、次のような結果が表示されます。

$ sudo apt instal python3-bpfcc
$ sudo python3 execve.py
(中略)
PID      PPID     COMM             FNAME
2822958  5480     tmux: server     /bin/sh
2822960  2822958  sh               /usr/bin/byobu-status
2822959  5480     tmux: server     /bin/sh
2822961  2822960  byobu-status     /usr/bin/sed
2822963  2822959  sh               /usr/bin/byobu-status
2822964  2822960  byobu-status     /usr/bin/tmux

sysfsの機能を用いてトレーシングする

シンプルなトレーシングなら余計なツールをインストールすることなく、カーネルのsysfsとシェルスクリプトだけでも十分です。たとえば単純にexecve()システムコールの呼び出しを記録する方法を実現してみましょう。ちなみにこの節の説明はbpftraceの利用とは関係ないため、bpftraceの使い方だけを知りたい場合は、読み飛ばしてもかまいません。

/sys/kernel/debug/tracingの活用

まずはsysfsを利用したトレーシングの基本からです。単純にexecve()システムコールの呼び出しを記録するだけなら、次の3行で実現できます。

$ sudo mkdir /sys/kernel/debug/tracing/instances/execve
$ echo 'sys_enter_execve' | sudo tee -a /sys/kernel/debug/tracing/instances/execve/set_event
$ echo 1 | sudo tee /sys/kernel/debug/tracing/instances/execve/events/syscalls/sys_enter_execve/enable

まず、/sys/kernel/debug/以下は管理者しかアクセスできません。タブ補完なども動かないので注意してください。面倒ならあらかじめsudo -iでrootになってしまうというのもひとつの手でしょう。

/sys/kernel/debug/tracing/instances以下に任意の名前のディレクトリを作ることで、個別のトレーサーを作成できます。さらにset_eventにはイベントトレーサーを記録できます。ここではシステムコールのsys_enter_execveを設定しています。利用可能なイベントの名前は/sys/kernel/debug/tracing/instances/execve/events/以下を見るといいでしょう。

最後に、登録したイベントを有効化してます。ここまですればトレースバッファーに結果が表示されます。あらかじめクリアしてから見ると、読みやすくなります。

$ echo 1 | sudo tee /sys/kernel/debug/tracing/instances/execve/free_buffer
$ sudo cat /sys/kernel/debug/tracing/instances/execve/trace_pipe
              sh-666208  [005] .... 1313488.957135: sys_execve(filename: 561bf66f3280, argv: 561bf57cd8c8, envp: 561bf66f3078)
    byobu-status-666209  [002] .... 1313488.958351: sys_execve(filename: 55d8463f94d0, argv: 55d846404450, envp: 55d8463f92c8)
    tmux: client-666211  [004] .... 1313488.959675: sys_execve(filename: 55d846406910, argv: 55d8464065f0, envp: 55d846406708)
(後略)

イベントごとの出力フォーマットは次の方法で確認できます。このあたりの書式や使い方はカーネルドキュメントのEvent Tracingを参照してください。

$ sudo cat /sys/kernel/debug/tracing/instances/execve/events/syscalls/sys_enter_execve/format
name: sys_enter_execve
ID: 707
format:
        field:unsigned short common_type;       offset:0;       size:2; signed:0;
        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
        field:int common_pid;   offset:4;       size:4; signed:1;

        field:int __syscall_nr; offset:8;       size:4; signed:1;
        field:const char * filename;    offset:16;      size:8; signed:0;
        field:const char *const * argv; offset:24;      size:8; signed:0;
        field:const char *const * envp; offset:32;      size:8; signed:0;

print fmt: "filename: 0x%08lx, argv: 0x%08lx, envp: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->argv)), ((unsigned long)(REC->envp))

この出力フォーマットを見る限り、残念ながらファイル名はポインタとしてしか表示してくれないようです。

作成したイベントを止め、削除するには次のコマンドを実行してください。

$ echo 0 | sudo tee /sys/kernel/debug/tracing/instances/execve/events/syscalls/sys_enter_execve/enable
$ echo '!sys_enter_execve' | sudo tee -a /sys/kernel/debug/tracing/instances/execve/set_event
$ sudo rmdir /sys/kernel/debug/tracing/instances/execve

/sys/kernel/debug/tracing/instances/execveディレクトリの削除は本当に不要になったタイミングで問題ありません。

システムコール以外をトレースしてファイル名を表示する

せっかくなのでファイル名を表示させてみましょう。この場合、syscallではなくカーネル内の関数を使ってKprobe Event Tracingを行います。まず対象となるカーネルの関数を決めます。

$ grep execve /proc/kallsyms
0000000000000000 t audit_log_execve_info
0000000000000000 t __do_execve_file.isra.0
0000000000000000 T __ia32_compat_sys_execve
0000000000000000 T __ia32_compat_sys_execveat
0000000000000000 T __ia32_sys_execve
0000000000000000 T __ia32_sys_execveat
0000000000000000 T __x32_compat_sys_execve
0000000000000000 T __x64_sys_execve
0000000000000000 T __x64_sys_execveat
0000000000000000 T __x32_compat_sys_execveat
0000000000000000 T do_execve_file
0000000000000000 T do_execve
0000000000000000 T do_execveat
0000000000000000 d event_exit__execveat
0000000000000000 d event_enter__execveat
(後略)

第690回でも少し触れたように、実行中のカーネルのシンボル名は/proc/kallsymsで確認できます。ちなみに最初のアドレスフィールドは管理者権限でアクセスしないと表示できません。2番目のフィールドはnmコマンドのマニュアルを参照してください。たとえばTだとテキストセクションにあるシンボルを意味し、大文字だとexternalで、小文字だとstaticになります。

システムコールのシンボルは環境に依存し、amd64なら__x64_sys_execveとなります。しかしながらシステムコールの引数を今回のようにkprobeで解釈するのはやっかいです。そこでシステムコールから呼ばれる関数を使いましょう。このあたりはカーネルのバージョンに依存するのですが、Ubuntu 20.04 LTSのカーネル5.4なら__do_execve_file.isra.0より新しいカーネルならdo_execveat_commonあたりが使えます。

$ echo 'p:execve __do_execve_file.isra.0 comm=$comm file=+0(+0($arg2)):string' | \
    sudo tee /sys/kernel/debug/tracing/kprobe_events
$ sudo cat /sys/kernel/debug/tracing/events/kprobes/execve/format
name: execve
ID: 1641
format:
        field:unsigned short common_type;       offset:0;       size:2; signed:0;
        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
        field:int common_pid;   offset:4;       size:4; signed:1;

        field:unsigned long __probe_ip; offset:8;       size:8; signed:0;
        field:__data_loc char[] comm;   offset:16;      size:4; signed:1;
        field:__data_loc char[] file;   offset:20;      size:4; signed:1;

print fmt: "(%lx) comm=\"%s\" file=\"%s\"", REC->__probe_ip, __get_str(comm), __get_str(file)

kprobe_eventsに対象となる関数とそのときに出力する内容を設定します。具体的な書式はKprobe Event Tracing「Synopsis of kprobe_eventsを参照してください。ここでは出てくるもののみを説明します。

最初にp:の後ろのexecveはイベント名です。/sys/kernel/debug/tracing/events/kprobes/以下にイベント名のディレクトリが作られます。今後はそのディレクトリ以下のファイルを操作することになります。

__do_execve_file.isra.0がターゲットとするシンボル名です。+offsの書式でオフセットを指定することも可能です。そこから先は可変長の引数として使います。ここの書き方は若干理解が難しいポイントです。

NAME=引数で引数にラベルがつけられてそれがそのまま出力されます。comm=$commの後者の$commは特殊な変数で、前述の「Synopsis of kprobe_eventsにあるように、カレントタスクのコマンド名となります。次のfile=+0(+0($arg2)):string__x64_sys_execveに渡されたファイル名を表示する部分です。まず、__do_execve_file.isra.0の定義を見てみましょう。

static int __do_execve_file(int fd, struct filename *filename,
                struct user_arg_ptr argv,
                struct user_arg_ptr envp,
                int flags, struct file *file)

ここで欲しいのは2番目の引数にあります。N番目の引数は$argNで指定できます[3]⁠。これはポインタになっているため、ポインタの先を参照するためには+0($argN)と表記します。+OFFS(ADDR)-OFFS(ADDR)で指定したアドレスに対する指定したオフセットのアドレスが示す先のメモリーにアクセスしてくれます。他にも+uOFFS(ADDR)とすることで、該当するアドレスをユーザーランドのアドレスとして扱います。

しかしながら2番目の引数の型であるstruct filenameは次のような構造体です。

struct filename {
    const char      *name;  /* pointer to actual string */
    const __user char   *uptr;  /* original userland pointer */
    int         refcnt;
    struct audit_names  *aname;
    const char      iname[];
};

つまり文字列を取得するためには、再度参照外しをしなくてはなりません。結果的に+0(+0($arg2))というアクセスになっています。最後に:stringと付けることでそのアドレスの型が文字列であることを指定しています。

さて実際にトレーサーを起動してみましょう。次のようにenableに1を書くことでトレーサーを起動できます。あとはシステムコール版と使い方は同じです。

$ echo 1 | sudo tee /sys/kernel/debug/tracing/events/kprobes/execve/enable
$ echo 1 | sudo tee /sys/kernel/debug/tracing/free_buffer
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
             cat-980     [000] ....  4396.341029: execve: (__do_execve_file.isra.0+0x0/0x840) comm="bash" file="/usr/bin/cat"
           <...>-981     [000] ....  4399.065705: execve: (__do_execve_file.isra.0+0x0/0x840) comm="bash" file="/usr/bin/cat"
            bash-982     [000] ....  4401.941474: execve: (__do_execve_file.isra.0+0x0/0x840) comm="bash" file="/usr/bin/ls"
            bash-983     [000] ....  4404.274790: execve: (__do_execve_file.isra.0+0x0/0x840) comm="bash" file="/usr/bin/touch"

無事にコマンド名と実行しょうとしているファイル名が表示されるようになりました。また、トレーサーを止めて消すには次のように実行します。

$ echo 0 | sudo tee /sys/kernel/debug/tracing/events/kprobes/execve/enable
$ echo '' | sudo tee /sys/kernel/debug/tracing/kprobe_events

上記の場合、登録済みのkprove_eventをすべて消してしまうので注意してください。

ちなみにシステムコールから先の関数として何が呼ばれるかはカーネルのバージョンやコンパイラのバージョンに依存します。たとえばUbuntu 21.04のカーネル5.10だとdo_execveat_common()を呼び出していることがわかります。よってより新しいカーネルだと次のようなコマンドになります。

$ echo 'p:execve do_execveat_common comm=$comm file=+0(+0($arg2)):string' | \
    sudo tee /sys/kernel/debug/tracing/kprobe_events

このようにsysfsを利用したトレーシングは、追加でパッケージをインストールできない環境で、ちょっと動作確認したい際にはこの方法がもっともお手軽で便利です。ただしカーネルのコードをある程度調べる必要があり、さらに複雑なトレーシングをしようとすると途端に難易度があがってしまいます。

bpftraceを用いてトレーシングする

ソフトウェアの依存関係を少なくトレーシングを行いたいならbpftraceも選択肢に入ってきます。パッケージのインストールは必要であるものの、直接sysfsを操作するよりははるかに簡単にトレーシングできます。

bpftraceパッケージはUbuntuリポジトリにもあるものの、最新版を使いたいならsnap版をインストールするのが便利です。

$ sudo snap install bpftrace
$ sudo snap connect bpftrace:system-trace

たとえばexecve()システムコールの呼び出し時に、呼び出し元のコマンド名とロードする予定のファイル名を出力してみましょう。

$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve
  { printf("%s %s\n", comm, str(args->filename)); }'
tmux: server /bin/sh
byobu-status /usr/bin/sed
sh /usr/bin/byobu-status
byobu-status /usr/bin/tmux

説明が不要なぐらいとても簡単ですね。-eは渡した文字列をeBPF用に評価するオプションです。そして文字列そのものはbpftrace独自の書式となります。まずはリファレンスガイドの前半を一通り読んでおくと、イメージがつきやすいでしょう。

bpftraceの書式の簡単な説明

まずはイベントやトレースポイントなどのプローブ対象の文字列を書きます。⁠execve」が含まれるプローブ対象は、-lオプションでリストアップ可能です。

$ sudo bpftrace -l '*execve*'
kprobe:__do_execve_file.isra.0
kprobe:__ia32_compat_sys_execve
kprobe:__ia32_compat_sys_execveat
kprobe:__ia32_sys_execve
kprobe:__ia32_sys_execveat
kprobe:__x32_compat_sys_execve
kprobe:__x32_compat_sys_execveat
kprobe:__x64_sys_execve
kprobe:__x64_sys_execveat
kprobe:audit_log_execve_info
kprobe:do_execve
kprobe:do_execve_file
kprobe:do_execveat
tracepoint:kprobes:execve
tracepoint:syscalls:sys_enter_execve
tracepoint:syscalls:sys_enter_execveat
tracepoint:syscalls:sys_exit_execve
tracepoint:syscalls:sys_exit_execveat

システムコールとしてはtracepoint:syscalls:sys_enter_execveがエントリーポイントとなり、sysfsのときのようにカーネルの内部関数であるkprobe:__do_execve_file.isra.0を使うことも可能です。

ちなみにUbuntu 20.10以降であれば、個々のプローブ対象の詳細を次のように確認できます。

$ sudo bpftrace -lv 'tracepoint:syscalls:sys_enter_execve'
ERROR: Permission denied: /proc/sys/kernel/randomize_va_space
tracepoint:syscalls:sys_enter_execve
    int __syscall_nr
    const char * filename
    const char *const * argv
    const char *const * envp

最初のエラーは、ASLR(Address Space Layout Randomization)が有効化どうかをチェックする際に、/proc/sys/kernel/randomize_va_spaceの読み込みに失敗したときに表示されます。snapパッケージの場合はこのファイルへのアクセスは許容されていないようです。実害はありませんが、気になるようなら実行時に環境変数BPFTRACE_CACHE_USER_SYMBOLS=0を明示的に指定しておくと良いでしょう。

eBPFの処理部分はAction Blocksの中に書くことになります。文は改行かセミコロンで区切ります。今回はprintfだけの単文のブロックです。プローブ対象の引数はargs->NAMEでアクセスできますし、sysfsのときのようにカレントタスクの情報はcommで取得できます。char *str()で文字列として使えるようになります。ただし設定しない限り、最大64文字です。

出力結果をわかりやすくする

せっかくなので、第690回のBCCでの出力結果に合わせてみましょう。

$ sudo bpftrace -e '#include <linux/sched.h>
  BEGIN { printf("PID      PPID     COMM             FNAME\n") }
  tracepoint:syscalls:sys_enter_execve {
    $task = (struct task_struct *)curtask;
    printf("%-8d %-8d %-16s %s\n", pid, $task->real_parent->tgid, comm, str(args->filename)); }'
Attaching 2 probes...
PID      PPID     COMM             FNAME
1968127  5480     tmux: server     /bin/sh
1968128  5480     tmux: server     /bin/sh
1968129  1968127  sh               /usr/bin/byobu-status
1968130  1968129  byobu-status     /usr/bin/sed
1968131  1968128  sh               /usr/bin/byobu-status
1968133  1968129  byobu-status     /usr/bin/tmux

こちらもほぼ説明が不要なシンプルなコードですね。ポイントは次のとおりです。

  • task_struct構造体のために、linux/sched.hヘッダーをインクルード
  • BEGINイベントでトレース開始時にヘッダーを出力
  • 組み込み変数であるcurtaskでカレントタスクの情報を取得し$taskとして保存
  • 同じく組み込み変数であるpidでスレッドグループのIDを取得
  • カレントタスクから$task->real_parent->tgidで親プロセスのスレッドグループIDを取得

このようにbpftraceを使えば、多少複雑な処理もワンライナーでかけてしまいます。ただしinclude文の後ろには改行が必要です。またbpftraceはスクリプトの中身をファイルとして扱うこともサポートしています。

$ cat <<'EOF' > execve.bt
#include <linux/sched.h>

BEGIN { printf("PID      PPID     COMM             FNAME\n") }
tracepoint:syscalls:sys_enter_execve {
  $task = (struct task_struct *)curtask;
  printf("%-8d %-8d %-16s %s\n", pid, $task->real_parent->tgid, comm, str(args->filename));
}
EOF

このように「execve.bt」という任意のファイル名でスクリプトの中身を保存しておけば、bpftraceコマンドにファイル名を渡すだけで、さきほどと同じトレースが実行されます。

$ bpftrace execve.bt
Attaching 2 probes...
PID      PPID     COMM             FNAME
2228     852      bash             /usr/bin/ls

つまりbpftraceは再利用性も十分確保されているのです。おそらくトレーシング目的であれば、現時点ではbpftraceがもっとも使いやすいのではないでしょうか。

おすすめ記事

記事・ニュース一覧