第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
今回は引数や呼び出し時刻等は表示していません。このあたりも取得・
このように、