🐦

入門 eBPF ①「Hello World」を通して、カーネルに耳をすませよう

に公開

今回の目的

  • Pythonの BCC(BPF Compiler Collection) ライブラリを使って、カーネル空間で「Hello World」を表示する最小のeBPFプログラムを実行してみます。
  • eBPFがどうやってカーネルの内部を観察できるのかを体感することが今回の目標です。

筆者の実行環境

  • 実行環境:UTM上の Linux(Ubuntu)
    → Mac上で仮想的にLinuxを動かして検証しています。
  • eBPFの動作確認にはroot権限が必要なため、実行時は sudo を使用します。

そもそもeBPFとは?

  • eBPF(extended Berkeley Packet Filter) とは、Linuxカーネル内でイベントが発生した際に、それに対応する処理を安全かつ柔軟に実行できる仕組みです。
  • カーネル空間で動作する仮想マシン(VM)のような存在で、現在はLinuxカーネルの一機能として提供されています。
  • ハイパーバイザのように実際のOSを仮想化するものではなく、
    専用の命令セットを持つ、仮想的なCPUとして動作します。
  • eBPF専用のバイトコード(VM上で動く中間コード)をカーネルに渡すと、そのコードは安全性の検証を経て、CPUが直接実行できるマシンコードへと変換・実行されます。
  • さらに詳しく知りたい方はこちら ↓

https://ebpf.io/ja/what-is-ebpf/

実際に動かしてみよう!

以下のコードをhello.pyとして保存し、実行してみます。

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

#カーネル側で動くeBPFプログラム(C言語風)
program = r"""
int hello(void *ctx) {
    bpf_trace_printk("Hello World!");
    return 0;
}
"""

#ここからはユーザー空間側の処理

#eBPFプログラムをコンパイルしてカーネルにロード
b = BPF(text=program)

#execve(新しいプロセス実行)の関数名を取得
syscall = b.get_syscall_fnname("execve")

#execve呼び出し時にhello()をフックします
b.attach_kprobe(event=syscall, fn_name="hello")

#カーネルログをリアルタイムに出力します
b.trace_print()

execve() を呼んでログを観測してみよう!

まず、今のターミナルからsudo python3 hello.py を実行します。
これでeBPFプログラムがカーネルにロードされ、execve() の呼び出しを監視し始めます。

次に、別のターミナルを並列で起動し、lsecho testなど、 プロセスを実行するコマンドを打ってみましょう。

すると、execve() が呼ばれるたびに
Hello Worldのログがカーネルから出力されるはずです。

出力例

[002] d..21  3175.804551: bpf_trace_printk: Hello Wor
sh-1254  ...

この出力を分解してみましょう!

部分 意味
[002] CPUの番号。マルチコアのうち「どのCPU上でイベントが起きたか」。この例だとCPU 2。
d..21 トレースのフラグ。スケジューラ関連の状態を表す。
3175.804551 イベントが発生したタイムスタンプ(秒)。起動後3175秒くらいに発生。
bpf_trace_printk: eBPFプログラム内で呼ばれたトレース出力関数。
Hello Wor... bpf_trace_printk("Hello World!") で出したメッセージ。途中で切れてるのは文字数制限のため。
sh-1254 イベントを起こしたプロセス名とPID。たとえば sh(シェル)プロセスのPID 1254。

つまり何を意味しているのか

  • これらの情報を通じて、カーネル内部でどんなプロセスがどのCPU上で動いているかを観察できます。

Discussion