Ubuntu Weekly Recipe

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

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

第688回のeBPFのコンパイラーに対応したツールでさまざまな挙動を可視化するではBPF Compiler Collectionに付属の各種サンプルツールの使い方を紹介しました。今回はコンパイラーを活用して,自分でeBPF用コードを書くための基礎を学んでみましょう。

BCCのインストールとドキュメント

第688回も紹介したように,カーネル3.15で追加されその後拡充を続けている「eBPF」は任意の外部プログラムをカーネルの中で,より安全に実行できる仕組みです。カーネルモジュールを作る代わりに,独自のバイトコードをコンパイラーで生成し,それをカーネル内部にロード・実行することになります。これを使えばシステムコールの先のカーネルの状態を,プログラマブルに解析可能になります。

eBPF自体はカーネルの仕組みであり,ユーザーランドから使うためには,eBPF用のバイトコードにコンパイルする必要があります。それを担ってくれるツールのひとつが,BPF Compiler Collection(BCC)です。前回同様,まずはBCCを導入しておきましょう。Ubuntu向けのパッケージとしては,apt版とsnap版の両方が用意されています。このうちsnap版はBCCが提供する各種ツールのみを提供しています。自分でプログラミングしたいなら,apt版のほうが必要です。

$ sudo apt instal bpfcc-tools

実際にコンパイルを行うのはPythonライブラリ側です。よって「BCCを使ってeBPFのコードを書く」場合,ほとんどのケースにおいて「Pythonプログラムの中でC言語のコードを書く」ような状態になります。純粋にC言語のみで書きたい場合は,LLVMなどを直接利用することになるでしょう。

ちなみにBCCを使ってeBPFのプログラムを書くだけであれば,実はbpfcc-toolsパッケージは不要です。本当に必要なのはpython3-bpfccパッケージとなります。ただしbpfcc-toolsに含まれる各種プログラムは実際のコーディングにあたって非常に参考になるため,特別な理由がない限りは一緒にインストールしておくと良いでしょう。

BCC向けのコードを書く場合,リファレンスガイドを参考にしながら記述していくことになります。リファレンスガイドには個別のAPIの説明だけでなく,そのAPIを利用したサンプルコードへのリンクも記述されているので,迷ったらまずはリファレンスガイドを参照すると良いでしょう。ここではリファレンスガイドを読むために必要な最低限の情報を説明していくことにします。

BCCではC言語でカーネル内部のロジックとデータの処理を実装し,それをPythonをスクリプトを使ってコンパイル・カーネルに流し込みます。このうちPython側にはC言語で記述する部分を肩代わりしてくれるラッパー機能も存在します。

  • BPF C:C言語部分のリファレンス
  • BCC Python:Python部分のリファレンス

kprobeで特定の関数呼び出し時の処理を追加する

まずはBCCのサンプルにあるhello_world.pyを読み解くことにしましょう。これはシステムコール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を想定して書かれたスクリプトが多いようです。よってそのまま実行権限を与えても,Python 2が/usr/bin/pythonが)インストールされていない環境だとうまく動かないかもしれません。ただしPython 3にも対応するよう書かれているため,とりあえずは「sudo python3 サンプルプログラム」のように実行すると良いでしょう。今回紹介するコードについては,Python 3で動かすことを想定しています。

さてまずはPython部分を説明します。BCC Pythonでは,カーネルの処理についてBPFと呼ばれるオブジェクト経由でeBPFのデータを処理します。ユーザーランドプログラムを処理する場合はUSDTを使います。BCCを使う場合はBPFかUSDTのどちらかのオブジェクトを作ることになるでしょう。

BPFの場合はインスタンス作成時のtextパラメーターで,eBPFのC言語部分を渡します。他にもsrc_fileでC言語のコードを直接渡すことも可能です。またcflags="文字列"で渡したコードをビルドする際のコンパイルオプションを,debug=数字でデバッグレベルも設定できます。

今回はtextを使って直接次のようなC言語のコードを指定しています。

int kprobe__sys_clone(void *ctx)
{
    bpf_trace_printk("Hello, World!\\n");
    return 0;
}

まずは関数名のkprobe__sys_cloneです。BCCではイベント名__関数名という書式で,カーネル内部の任意の関数のイベントをフックして処理を追加できるようになっています。このタイプで記述できるイベントはkprobeskretprobesの2種類です。前者が関数が呼び出される前に実行されるイベントで,後者が関数から戻る時に実行されるイベントです。

今回だとkprobe__sys_cloneという名前であるため,sys_clone()が呼び出される時に指定したコードが実行されることになります。kprobe/kretprobesでは必ずBPFコンテキストのレジスターを保存するstruct pt_regs *ctxを第一引数として指定します。今回は使用しないためvoid *としています。また,任意の数の対象関数の引数を渡すことも可能です。

関数の中ではカーネルのAPIやBCCのAPIを呼び出せます。bpf_trace_printk()はBCC側のAPIで,/sys/kernel/debug/tracing/trace_pipeに指定した文字列を出力します。ただし引数は最大3個とか,%sは1個までとか,並列処理すると出力が混じってしまうとかいろいろ制約が存在する点については注意が必要です。⁠とりあえず出力してみたい」という用途にのみ有用で,本格的にはBPF_PERF_OUTPUTなどを使うことになるのでしょう。

これでC言語の部分の説明は完了です。最後に残ったのはBPF().trace_print()だけです。trace_print()bpf_trace_printk()trace_pipeに出力されたデータを読み込んで表示するだけの関数です。

実際にこのコードを実行してみましょう。

$ 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_textとしてC言語部分をヒアドキュメント化してみました。長めのコードを書くなら,この形式のほうが読みやすいでしょう。

bpf_get_current_comm()はカレントタスクのプログラム名を文字列にコピーしてくれるBCCのAPIです。あとは%sで表示すれば完了となります。もともとtrace_pipeにはタスク名が表示されるため,情報量は変わりませんが雰囲気はわかるかと思います。

このようにeBPFのC言語部分は,普通のC言語のプログラムとして拡充できます。

著者プロフィール

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

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