第688回の
BCCのインストールとドキュメント
第688回も紹介したように、
eBPF自体はカーネルの仕組みであり、
$ sudo apt instal bpfcc-tools
実際にコンパイルを行うのはPythonライブラリ側です。よって
ちなみにBCCを使ってeBPFのプログラムを書くだけであれば、
BCC向けのコードを書く場合、
BCCではC言語でカーネル内部のロジックとデータの処理を実装し、
- BPF C:C言語部分のリファレンス
- BCC Python:Python部分のリファレンス
kprobeで特定の関数呼び出し時の処理を追加する
まずはBCCのサンプルにあるhello_
」clone(2)
」Hello, World!
」
from bcc import BPF
BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; }').trace_print()
ちなみにBCCのサンプルはPython 2を想定して書かれたスクリプトが多いようです。よってそのまま実行権限を与えても、/usr/
が)
さてまずはPython部分を説明します。BCC Pythonでは、
BPFの場合はインスタンス作成時のtext
」src_
」cflags="文字列"
」debug=数字
」
今回はtext
を使って直接次のようなC言語のコードを指定しています。
int kprobe__sys_clone(void *ctx)
{
bpf_trace_printk("Hello, World!\\n");
return 0;
}
まずは関数名のkprobe__
」イベント名__関数名
」
今回だとkprobe__
」sys_
」struct pt_
」void *
」
関数の中ではカーネルのAPIやBCCのAPIを呼び出せます。bpf_
はBCC側のAPIで、/sys/
に指定した文字列を出力します。ただし引数は最大3個とか、%s
」BPF_
などを使うことになるのでしょう。
これでC言語の部分の説明は完了です。最後に残ったのはBPF().trace_
」trace_
」bpf_
でtrace_
に出力されたデータを読み込んで表示するだけの関数です。
実際にこのコードを実行してみましょう。
$ sudo python3 hello_world.py (BCCによるコンパイルログ) b' tmux: server-5480 [001] d... 626046.065863: bpf_trace_printk: Hello, World!' b'' b' tmux: server-5480 [001] d... 626046.071380: bpf_trace_printk: Hello, World!' b''
バックグラウドで何かプログラムが動くたびにHello, World!
」
TASK PID CPU FLAG TIMESTAMP FUNCTION b' tmux: server-5480 [001] d... 626046.065863: bpf_trace_printk: Hello, World!'
追加でタスク名を表示してみる
たとえばタスクの名前を、
from bcc import BPF
bpf_text="""
#include <linux/sched.h>
int kprobe__sys_clone(void *ctx)
{
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("Hello, World!: %s\\n", comm);
return 0;
}
"""
BPF(text=bpf_text).trace_print()
今回はbpf_
としてC言語部分をヒアドキュメント化してみました。長めのコードを書くなら、
bpf_
はカレントタスクのプログラム名を文字列にコピーしてくれるBCCのAPIです。あとは%s
」trace_
にはタスク名が表示されるため、
このようにeBPFのC言語部分は、
出力フォーマットをカスタマイズする
trace_
をそのまま表示するだけだと表示が複雑になってしまうため、
最初に、trace_
は引数から出力するフィールド値やフォーマットを書き換えられます。
trace_print(fmt="TASK={0} PID={1} MESSAGE={5}")
これだけでも、
TASK=b'tmux: server' PID=5480 MESSAGE=b'Hello, World!: tmux: server' TASK=b'byobu-status' PID=2597659 MESSAGE=b'Hello, World!: byobu-status'
さらにtrace_
を使えば、
from bcc import BPF
bpf_text="""
#include <linux/sched.h>
int kprobe__sys_clone(void *ctx)
{
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("Hello, World!: %s\\n", comm);
return 0;
}
"""
b = BPF(text=bpf_text)
while True:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
print("PID={}, CPU={}, MSG={}".format(pid, cpu, msg.decode()))
except KeyboardInterrupt:
exit()
except ValueError:
continue
これまではBPFで生成したオブジェクトから直接メソッドを呼び出していましたが、b
」
b.
はtrace_
から1行読み込み、trace_
のフォーマットに準じたタプル型に変換してくれます。たとえばtaskはバイトオブジェクトになりますし、
ここではバイトオブジェクトはprint()したときの見た目のためにdecode()
メソッドで文字列に変換しています。また、trace_
を変換できなかったときは無視しています。結果的に、
PID=2723064, CPU=3, MSG=Hello, World!: tmux: server PID=2723066, CPU=7, MSG=Hello, World!: byobu-status PID=2723065, CPU=3, MSG=Hello, World!: byobu-status
PythonAPIを用いて再利用性を高める
ここまでの例だと、
たとえばattach_
」
from bcc import BPF
bpf_text="""
#include <linux/sched.h>
int hello(void *ctx)
{
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("Hello, World!: %s\\n", comm);
return 0;
}
"""
b = BPF(text=bpf_text)
b.attach_kprobe(event="__x64_sys_clone", fn_name="hello")
b.attach_kprobe(event="__x64_sys_execve", fn_name="hello")
b.trace_print(fmt="TASK={0} PID={1} MESSAGE={5}")
C言語部分の関数名が変わっていることに注意してください。これはattach_
側のfn_
に渡す名前で、clone()
だけでなくexecve()
もトレースの対象にしてみました。
__
はamd64環境におけるclone(2)
の表記方法です。環境ごとの名前は/proc/
で確認できます。また、sys_
を使う場合、
cannot attach kprobe, probe entry may not exist Traceback (most recent call last): File "/home/shibata/temp/bpfcc/hello_world.py", line 24, in <module> b.attach_kprobe(event="sys_clone", fn_name="hello") File "/usr/lib/python3/dist-packages/bcc/__init__.py", line 683, in attach_kprobe raise Exception("Failed to attach BPF program %s to kprobe %s" % Exception: Failed to attach BPF program b'hello' to kprobe b'sys_clone'
要するにこの環境だとsys_
」
$ sudo tail /sys/kernel/debug/tracing/error_log [644916.964850] trace_kprobe: error: Invalid probed address or symbol Command: p:kprobes/p_sys_clone_bcc_2652701 sys_clone ^
これはカーネルのバージョンによって起こりうる問題です。環境ごとの名前は先ほど言及したように、/proc/
で確認できます。ただしこれを使うと、get_
です。このメソッドを使うと、
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="hello")
Python側へ文字列以外のデータを通知する
ここまではカーネルとユーザーランドのやりとりは文字列のみで行っていました。つまりカーネル側の処理はtrace_
に文字列として結果を保存し、
そこで最後の例として、execve(2)
が呼び出されたときに、
実際のコードの内容
先にコード全体を掲載しておきます。これは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()
最後の例では、
システムコール特有の話
まずattach_
のfn_
がhello
ではなくsyscall__
になりました。これはsystemcall trace pointという書式で、
システムコール以外のカーネルの関数をフックするだけなら、
C言語部分の解説
次にC言語部分を見ていきます。data_
構造体は、
BPF_
が重要ポイントです。これにより、perf_
を使ってデータを登録していきます。
ちなみにより高機能なAPIとしてBPF_
も存在します。性能も向上しているみたいで、BPF_
からBPF_
への移行は、
syscall__
では各種データの情報を取得した上で、events.
でdata_
構造体のデータをバッファーに記録しています。
- PIDは
bpf_
で取得しています。今回はスレッドグループのIDを表示する想定で、get_ current_ pid_ tgid() 上位32bitの値を使っています。 - PPIDは
bpf_
で取得したタスク構造体から取得しています。get_ current_ task - タスク名の取得方法はこれまでと同じ
bpf_
です。get_ current_ comm()
ファイル名が今回のポイントその2です。このファイル名はexecve(2)
の引数であり、syscall__
の引数に、execve(2)
の引数と同じconst char __
」bpf_
を用いて、
ちなみに今回はユーザー空間の文字列だけでしたが、bpf_
が使えます。
これでC言語側の準備はできました。これによりexecve(2)
が呼ばれるたびに、data_
構造体のデータがリングバッファーに保存されることになります。次はそれを取り出すPython側のコードです。
Python部分の解説
説明の前に、
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()
BPF_
で作成したBPFテーブルは、BPF["テーブル名"]
」b["events"]
」
まずopen_
で、print_
ですね。BPF_
はCPUごとにバッファーが作られるため、
コールバック関数の中ではb["events"].event(data)
」bcc.
型となります。
最後にperf_
です。これにより開かれているリングバッファーのいずれかにデータが届いたら、timeout=
」
そのほか、
実行結果
ここまでで最後のサンプルの説明は終えました。実際に動かしてみると、
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
今回は引数や呼び出し時刻等は表示していません。このあたりも取得・
このように、