Ubuntu Weekly Recipe

第690回 BCCでeBPFのコードを書いてみる

この記事を読むのに必要な時間:およそ 7 分

出力フォーマットをカスタマイズする

trace_pipeをそのまま表示するだけだと表示が複雑になってしまうため,もう少し設定できるようにしてみましょう。

最初に,trace_print()は引数から出力するフィールド値やフォーマットを書き換えられます。

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_fields()を使えば,Pythonの変数に分解できます。

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_fileds()trace_pipeから1行読み込み,それをtrace_pipeのフォーマットに準じたタプル型に変換してくれます。たとえばtaskはバイトオブジェクトになりますし,pidやcpu,tsは周囲の装飾文字を刈り取った上で数値型に変換してくれます。あとはPythonの流儀に合わせて使えるというわけです。

ここではバイトオブジェクトはprint()したときの見た目のためにdecode()メソッドで文字列に変換しています。また,キーボード割り込みがきたときは終了し,trace_pipeを変換できなかったときは無視しています。結果的に,その出力は次のようにより読みやすくなりました。

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を用いて再利用性を高める

ここまでの例だと,トレースしたいカーネル関数ごとに関数を増やさなくてはなりません。そこでBCCのPythonバインディングを活用して,再利用性を高めましょう。

たとえばattach_kprobe()は指定した関数を,特定のカーネル関数のkprobeイベントに紐付けてくれるAPIです。これを使えば,複数のカーネル関数に同じ処理を行うコードを簡単に書けます。

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_kprobe()側のfn_nameに渡す名前で,他と被らない限りは任意の名前を利用可能です。また,今回はclone()だけでなくexecve()もトレースの対象にしてみました。

__x64_sys_cloneはamd64環境におけるclone(2)の表記方法です。環境ごとの名前は/proc/kallsymsで確認できます。また,単純にsys_cloneを使う場合,環境によっては次のようなエラーになります。

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_cloneという名前の関数はカーネルに存在しないということです。実際BCC側がカーネルに問い合わせたときのエラーが次のように残っています。

$ 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/kallsymsで確認できます。ただしこれを使うと,環境に依存したコードになってしまいます。そこで便利なのがget_syscall_fnname()です。このメソッドを使うと,該当部分は次のように書き直せます。

b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="hello")

著者プロフィール

柴田充也(しばたみつや)

Ubuntu Japanese Team Member株式会社 創夢所属。数年前にLaunchpad上でStellariumの翻訳をしたことがきっかけで,Ubuntuの翻訳にも関わるようになりました。