Skip to content
Published on

eBPF 完全ガイド — カーネル内の小さな仮想マシン: Verifier、JIT、CO-RE、Maps、Attach Points、XDP、LSM、sched_ext (2025)

Authors

はじめに — eBPF は Linux の新しい神経系である

eBPF を初めて知った人の反応は大抵「これ、本当に可能なの?」というものだ。ユーザー空間のプログラムがカーネル内部の任意の地点に小さなコード片を差し込み、そのコードがカーネルのデータ構造を安全に読み取り、結果をユーザー空間に返す。 しかも、そのコードは verifier を通過しないと実行できないため、不正なコードがカーネルを壊すことはできない。

これは 30 年前なら狂気の沙汰に聞こえただろう。しかし 2025 年の Linux は、まさにこのモデルの上で監視、セキュリティ、ネットワーキング、そしてスケジューリングまでも拡張している。Cilium は Kubernetes ネットワーキング全体を eBPF で書き直した。Falco と Tetragon は eBPF でランタイムセキュリティを実現している。Datadog のシステムメトリクス収集エージェントは eBPF に依存する。Linux 6.12 では eBPF がスケジューラまで拡張された (sched_ext)。

この記事は、eBPF を初めて聞く人から、すでに BCC ツールを使ったことがある人までを対象とする。1992 年の cBPF の 14 行の ISA から始まり、2014 年の Alexei Starovoitov による最初の eBPF パッチ、verifier の魔法、JIT コンパイル、CO-RE、そしてモダン Linux のすべての attach point までを 1,400 行で整理する。

この記事は Linux 内部構造シリーズと姉妹作だ。シリーズが「カーネルは何をするのか」を扱ったのに対し、この記事は「ユーザーがカーネルをどう拡張できるのか」を扱う。


1. 歴史 — cBPF から eBPF へ

1.1 1992 — Berkeley Packet Filter

1992 年、Steven McCanne と Van Jacobson は BSD OS 向けの新しいパケットフィルタリング機構を発表した。以前のパケットフィルタ (CSPF) は木ベースの式で、非常に遅かった。彼らの新モデルは シンプルな仮想マシン だった:

  • 32 ビットのアキュムレータ (A) とインデックスレジスタ (X)
  • 16 個の 32 ビットスクラッチメモリスロット
  • パケットからデータを読んだりスロットと比較したりするシンプルな命令群
  • ジャンプ命令で決定木を表現

このモデルは BSD で大成功を収め、すぐに Linux と Solaris にも移植された。tcpdump や libpcap はこの上で動く。tcp port 80 という式は内部的に約 20 個の cBPF 命令にコンパイルされる。

cBPF は 30 年間、実質的に変わらなかった。シンプルで、うまく動き、パケットフィルタリングという狭い領域では十分だった。

1.2 2013 — Alexei Starovoitov の最初のパッチ

2013 年、PLUMgrid に在籍していた Alexei Starovoitov は LKML に大きなパッチを投稿した。タイトルは「extended BPF」。主要な変更:

  • 32 ビット → 64 ビットレジスタ
  • 2 個 → 11 個のレジスタ (R0-R10)
  • 関数呼び出し命令
  • より多くの算術・ビット演算
  • x86_64 に非常に近い ISA — JIT コンパイルがほぼ 1:1

当初は懐疑的な反応が多かった。「なぜ BPF を拡張するのか? あれはパケットフィルタ用ではないのか?」しかし Alexei のビジョンはもっと大きかった — ユーザー空間が安全にカーネル内で実行できる小さなコードのための汎用インターフェース

1.3 2014 — Linux 3.18 メインライン

2014 年 9 月、最初の eBPF パッチが Linux 3.18 にマージされた。当初は socket filter 用途だった (cBPF の直接の後継)。しかしすぐに、リリースごとに新しい attach point が追加されていった:

  • 2014 (3.18): 基本インフラ、socket filter
  • 2015 (3.19): kprobe attach
  • 2015 (4.1): tc (traffic control) attach
  • 2016 (4.4): tracepoint attach
  • 2016 (4.8): XDP attach
  • 2017 (4.10): cgroup attach、perf_event attach
  • 2018 (4.15): BTF 導入 (CO-RE の土台)
  • 2018 (4.18): socket lookup
  • 2019 (5.7): LSM attach (KRSI)
  • 2020 (5.8): BPF_RINGBUF
  • 2021 (5.13): スタックトレース BPF helper
  • 2022 (5.15): bpf_loop helper
  • 2023 (6.4): BPF スタックウォーキング改善
  • 2024 (6.12): sched_ext メインラインマージ

今日の eBPF はほぼあらゆるカーネルサブシステムと統合されている。30 年前はパケットフィルタ用の小さな仮想マシンだったものが、Linux の運用モデルそのものの一部を変えた。

★ Insight ─────────────────────────────────────

  • 名前の混乱: 「eBPF」は公式名称ではない。カーネルコードでは単に「BPF」と呼ぶ。外部では「extended BPF」の略「eBPF」のほうが一般的。古い BPF は「classic BPF」または「cBPF」として区別する。
  • なぜ「パケットフィルタ」が汎用仮想マシンになったのか: 核心的な洞察は、「検証された安全なコードをカーネル内で実行する」モデルがパケットフィルタだけでなくほぼすべてのカーネル拡張に適用できるということだった。cBPF が持っていた「自己終了保証 + メモリ安全性」という性質をより豊かな ISA に持ち込んだのが eBPF。
  • Linux 以外にはほぼない: BSD 陣営には一部の eBPF 移植があるが (DTrace を持つ BSD は eBPF の魅力が薄い)、Windows は 2021 年から ebpf-for-windows という別プロジェクトを持っている。とはいえ「eBPF = 実質 Linux」と言ってもほぼ正しい。 ─────────────────────────────────────────────────

2. eBPF 仮想マシン — ISA とレジスタ

2.1 11 個の 64 ビットレジスタ

eBPF VM は非常にシンプルだ:

  • R0: 関数の戻り値、プログラムの終了値
  • R1 ~ R5: 関数引数 (1-5)
  • R6 ~ R9: callee-saved
  • R10: スタックフレームポインタ (read-only)

このインターフェースは意図的に x86_64 の calling convention に非常に近い。JIT コンパイルが 1:1 に近くなる。

2.2 スタック

各プログラムは 512 バイトのスタックを持つ。R10 がその base を指す。小さく見えるが、verifier がすべての使用を追跡しなければならないため小さく取られている。

2.3 命令エンコード

eBPF 命令は 64 ビット固定サイズ:

+----+----+----+--------+
| op | dst | src | offset | imm |
| 8  | 4  | 4  | 16     | 32  |
+----+----+----+--------+
  • op: 8 ビット opcode
  • dst/src: 4 ビットのレジスタインデックス
  • offset: 16 ビット符号付きオフセット (ジャンプ、メモリアクセス)
  • imm: 32 ビット即値

2.4 命令カテゴリ

コアカテゴリ:

  1. ALU: 算術・論理演算 (32 ビットと 64 ビット)
  2. Memory: load/store
  3. Branch: 条件ジャンプ
  4. Call: helper 関数呼び出し
  5. Exit: プログラム終了

例:

BPF_MOV64_IMM(BPF_REG_0, 0)     // R0 = 0
BPF_MOV64_REG(BPF_REG_1, BPF_REG_10)  // R1 = R10 (frame pointer)
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -8) // R1 += -8
BPF_LD_MAP_FD(BPF_REG_1, map_fd)      // R1 = map fd
BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem) // call helper
BPF_EXIT_INSN()                  // exit

2.5 200 行の ISA

eBPF ISA 全体は約 200 行の C コードで表現可能 (include/uapi/linux/bpf.h 参照)。非常に小さい。しかしこの小さな ISA で非常に豊かなことができる。


3. eBPF プログラムのライフサイクル

3.1 記述 — C から BPF バイトコードへ

典型的な流れ:

// hello.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

char LICENSE[] SEC("license") = "GPL";

SEC("kprobe/sys_open")
int hello(struct pt_regs *ctx) {
    char fmt[] = "Hello from kprobe!\n";
    bpf_trace_printk(fmt, sizeof(fmt));
    return 0;
}

コンパイル:

clang -O2 -target bpf -c hello.bpf.c -o hello.bpf.o

-target bpf が鍵。clang の BPF バックエンドが eBPF 命令にコンパイルする。出力は ELF ファイルで、その中に BPF バイトコードが入っている。

3.2 ロード — bpf() システムコール

ELF からバイトコードを取り出して bpf(BPF_PROG_LOAD, ...) システムコールでカーネルに送る。libbpf がこの過程をカプセル化する:

struct bpf_object *obj = bpf_object__open("hello.bpf.o");
bpf_object__load(obj);  // verify + JIT
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "hello");
bpf_program__attach(prog);  // kprobe に attach

3.3 Verifier

bpf(BPF_PROG_LOAD, ...) が呼ばれると、カーネルはまず verifier を実行する。verifier はプログラムが安全かを検査する:

  • すべてのメモリアクセスが有効か
  • すべてのジャンプが定義済みの位置へ行くか
  • 無限ループはないか
  • helper 呼び出しが適切なコンテキストで行われているか
  • スタック使用量が制限内か

verifier は一般に 5 段階で進む (次節で詳述)。

3.4 JIT コンパイル

検証を通過すると、JIT コンパイラが BPF バイトコードをネイティブ機械語にコンパイルする。x86_64 ではほぼ 1:1 マッピング。ARM64 でも同様。

JIT はオプションだが、ほぼすべてのモダンなシステムで有効になっている (net.core.bpf_jit_enable=1)。インタプリタより約 10-100 倍速い。

3.5 Attach

JIT 済みのプログラムを特定の attach point に接続する。例えば kprobe/sys_open という SEC 名は sys_open 関数のエントリーポイントに kprobe を張るという意味。libbpf が BPF_PROG_ATTACH システムコールを呼んで接続する。

この時点から sys_open が呼ばれるたびに BPF プログラムが実行される。


4. Verifier — eBPF の本当の魔法

4.1 何を保証するのか

verifier は次のことを静的に保証する:

  1. メモリ安全性: すべての load/store が有効な領域
  2. 型安全性: ポインタを整数のように扱わない
  3. 終了保証: 無限ループなし (または明示的な bound)
  4. スタック安全性: スタックオーバーフローなし
  5. 呼び出し安全性: helper 呼び出しが適切な引数とコンテキストで行われる

これらすべてを、コードを実行せずに静的解析で保証する。非常に難しい問題だ。

4.2 どう動くのか — 抽象解釈

verifier の核となるアルゴリズムは 抽象解釈 (abstract interpretation)。すべての可能な実行経路をシミュレーションしつつ、各時点のレジスタ・スタックの状態を「抽象値」で追跡する。

抽象値の例:

  • R0 = SCALAR_VALUE, range [0, 100]
  • R1 = PTR_TO_MAP_VALUE, off 0..16
  • R2 = PTR_TO_PACKET, off 14..1500
  • R3 = NOT_INIT

各命令を処理しながらこの抽象値を更新する。分岐では両経路を探索。

4.3 path explosion の回避 — pruning

素直にすべての経路を探索すると指数爆発が起きる。verifier はすでに見た状態を記憶し (states_cache)、同じ状態に再進入すればその経路を刈り込む。

これは非常に効果的だが、複雑なプログラムではそれでも検証に 10 秒以上かかることがある。検証状態数が 100 万を超えると verifier は諦める (-EFBIG または -ENOSPC)。

4.4 メモリアクセスの検証

int *p = bpf_map_lookup_elem(&my_map, &key);
*p = 42;  // 検証失敗!

このコードは検証に失敗する。bpf_map_lookup_elem は NULL を返しうるのに、NULL チェックなしにデリファレンスしているためだ。正しいコード:

int *p = bpf_map_lookup_elem(&my_map, &key);
if (p) {
    *p = 42;  // OK
}

verifier は if (p) を見て、その分岐の中では p が NULL でないことを追跡する。だから *p アクセスが安全だとわかる。

4.5 終了保証

ループは verifier の悩みの種だ。eBPF は当初、ループを完全に禁止していた。すべてのループはコンパイル時に unroll されなければならなかった。

5.3 から bounded loops が許可された。verifier がループ回数が有限であることを証明できれば OK:

#pragma unroll
for (int i = 0; i < 10; i++) {
    /* ... */
}

5.13 からは bpf_loop helper が追加され、より柔軟なループが可能になった:

static int callback(__u32 idx, void *data) {
    /* ... */
    return 0;  // 0 = continue, 1 = stop
}
bpf_loop(1000, callback, &my_data, 0);

4.6 スタック使用量

各プログラムは 512 バイトのスタックを持つ。大きな構造体をスタックに置くとすぐに制限を超える。

SEC("kprobe/sys_open")
int hello(struct pt_regs *ctx) {
    char buf[600];  // 検証失敗! スタック制限超過
    return 0;
}

代替案: BPF_MAP_TYPE_PERCPU_ARRAY を「大きなスクラッチ領域」として利用する。

4.7 Verifier のデバッグ

verifier が失敗すると非常に長いエラーメッセージが出る。bpftool prog show + verifier_log_level=2 で詳細を見られる。各命令と共に verifier の抽象状態が一行ずつ出力される:

0: (b7) r1 = 0
   R1_w=0 R10=fp0
1: (61) r2 = *(u32 *)(r1 +0)
   R1 invalid mem access 'inv'
processed 2 insns ...

このログを読める能力が eBPF 開発者の核心スキルだ。


5. BPF Maps — ユーザー空間との通信

5.1 なぜ必要か

BPF プログラムは揮発的だ — 呼び出しが終わるとすべてのローカル状態が消える。データを永続化したりユーザー空間と共有したりするには別の仕組みが必要だ。それが BPF Maps。

Maps はキー・バリューストア。BPF プログラムは helper 関数で、ユーザー空間はシステムコールでアクセスする。

5.2 Map 種類 (17 種以上)

コアな種類:

種類用途
BPF_MAP_TYPE_HASHハッシュテーブル (最も一般的)
BPF_MAP_TYPE_ARRAY固定サイズ配列、インデックスベース
BPF_MAP_TYPE_PERCPU_HASHCPU ごとのハッシュ (ロックなし)
BPF_MAP_TYPE_PERCPU_ARRAYCPU ごとの配列
BPF_MAP_TYPE_LRU_HASHLRU eviction 付きハッシュ
BPF_MAP_TYPE_LPM_TRIELongest prefix match trie (ルーティング用)
BPF_MAP_TYPE_PROG_ARRAYBPF プログラム配列 (tail call 用)
BPF_MAP_TYPE_PERF_EVENT_ARRAYperf イベント出力用
BPF_MAP_TYPE_RINGBUF新しい ring buffer (5.8+)
BPF_MAP_TYPE_QUEUEFIFO
BPF_MAP_TYPE_STACKLIFO
BPF_MAP_TYPE_SK_STORAGEsocket ごとのストレージ
BPF_MAP_TYPE_TASK_STORAGEtask ごとのストレージ
BPF_MAP_TYPE_INODE_STORAGEinode ごとのストレージ
BPF_MAP_TYPE_CGROUP_STORAGEcgroup ごとのストレージ
BPF_MAP_TYPE_BLOOM_FILTERbloom filter
BPF_MAP_TYPE_USER_RINGBUFユーザー → カーネル ringbuf (5.19+)

5.3 Hash Map の例

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, __u32);
    __type(value, __u64);
} counter_map SEC(".maps");

SEC("kprobe/sys_open")
int count_opens(struct pt_regs *ctx) {
    __u32 pid = bpf_get_current_pid_tgid() >> 32;
    __u64 *count = bpf_map_lookup_elem(&counter_map, &pid);
    if (count) {
        __sync_fetch_and_add(count, 1);
    } else {
        __u64 init = 1;
        bpf_map_update_elem(&counter_map, &pid, &init, BPF_ANY);
    }
    return 0;
}

このプログラムは sys_open 呼び出しのたびに PID ごとのカウンタを増やす。ユーザー空間では bpf_map__lookup_elem で同じ map を参照する。

5.4 PERCPU バリアント — ロックなしカウンタ

BPF_MAP_TYPE_PERCPU_HASH は CPU ごとに別のハッシュテーブルを持つ。ロックは不要 — 同じ CPU の BPF プログラムしかその CPU の map にアクセスしないからだ。

ユーザー空間が PERCPU map を読むときは、すべての CPU の値を受け取る。集計はユーザー空間の責任。

PERCPU map はカウンタ、統計、ヒストグラムに非常に有用。ロック競合がなく非常に速い。

5.5 RINGBUF — 新しいイベント出力

伝統的に BPF プログラムがユーザー空間へイベントを送るときは BPF_MAP_TYPE_PERF_EVENT_ARRAY を使った。これは CPU ごとの perf リングバッファで、各 CPU に別々の ring を持つ。

5.8 で導入された BPF_MAP_TYPE_RINGBUF はより洗練されている:

  • 単一の共有 ring buffer (CPU 間で分割なし)
  • メモリ使用量が少ない
  • BPF プログラムが可変サイズのイベントを直接 reserve/commit できる
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("tp/sched/sched_process_exec")
int on_exec(struct trace_event_raw_sched_process_exec *ctx) {
    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;

    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));

    bpf_ringbuf_submit(e, 0);
    return 0;
}

新しい BPF ツールのほとんどは ringbuf を使う。perf event array は互換性のために残っている。


6. Helper 関数 — 200 を超えるカーネルインターフェース

6.1 Helper とは

BPF プログラムは任意のカーネル関数を呼べない。代わりに、検証済みの「helper 関数」集合のみ呼び出せる。helper はカーネルが明示的に露出した安全なインターフェースだ。

5.0 時点で約 100 個だった helper は 6.x 時点で 200 個をゆうに超える。

6.2 コアな helper

最もよく使うもの:

helper用途
bpf_map_lookup_elemmap から値を読む
bpf_map_update_elemmap に値を書く
bpf_map_delete_elemmap から値を削除する
bpf_get_current_pid_tgid現在の PID/TID
bpf_get_current_uid_gid現在の UID/GID
bpf_get_current_comm現在のプロセス名
bpf_get_current_task現在の task_struct ポインタ
bpf_ktime_get_ns単調時間 (ナノ秒)
bpf_trace_printkデバッグ printf (/sys/kernel/debug/tracing/trace_pipe)
bpf_perf_event_outputperf event array へイベント出力
bpf_ringbuf_reserve/bpf_ringbuf_submitringbuf 出力
bpf_get_stack/bpf_get_stackidスタックトレース
bpf_probe_read_kernel/bpf_probe_read_user安全なメモリ読み取り
bpf_skb_load_bytesパケットからバイトを読む
bpf_redirectパケットリダイレクト
bpf_xdp_adjust_headXDP パケットヘッダ調整
bpf_jiffies64現在の jiffies
bpf_send_signalシグナル送信 (5.3+)

6.3 helper の安全性

各 helper は自身の引数型を明示する。verifier が呼び出し時に引数型を検査する。

例えば bpf_map_lookup_elem のシグネチャ:

void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)

verifier は mapBPF_MAP_TYPE_HASH のような型のポインタか、key がその map の key_size 分のメモリを指しているかを確認する。

6.4 GPL と非 GPL helper

一部の helper は GPL 専用だ。非 GPL ライセンスの BPF プログラムは呼び出せない。bpf_trace_printk がそれに当たる (デバッグ用なので)。

char LICENSE[] SEC("license") = "GPL";  // GPL helper 使用可能

商用 BPF ツールのほとんどは GPL だ。


7. Attach Points — どこに付けられるか

eBPF の本当の力は多様な attach point から来る。各 attach point は自分だけの「context」(引数) と helper 集合を持つ。

7.1 kprobe / kretprobe

ほぼあらゆるカーネル関数のエントリ・リターン地点に attach できる。

SEC("kprobe/vfs_read")
int on_vfs_read(struct pt_regs *ctx) {
    /* ... */
    return 0;
}

エントリでは pt_regs を通じて引数にアクセス。リターンでは PT_REGS_RC(ctx) で戻り値にアクセス。

利点: ほぼあらゆるカーネル関数に付けられる。 欠点: 関数シグネチャがカーネルバージョンごとに異なる場合がある。CO-RE で一部緩和。

7.2 fentry / fexit (BPF Trampoline)

5.5 で導入されたより高速な代替。kprobe は INT3 命令を差し込むが、fentry/fexit は ftrace インフラを活用して直接呼び出す。約 10 倍速い。

SEC("fentry/vfs_read")
int BPF_PROG(on_vfs_read_entry, struct file *file, char *buf, size_t count) {
    /* BTF のおかげで引数に直接アクセス可能 */
    return 0;
}

7.3 tracepoint

カーネルコードに予め埋め込まれた安定したトレース地点。kprobe と違い、関数名が変わっても壊れない。

SEC("tp/sched/sched_process_exec")
int on_exec(struct trace_event_raw_sched_process_exec *ctx) {
    /* tracepoint 引数に直接アクセス */
    return 0;
}

/sys/kernel/debug/tracing/events/ で利用可能なすべての tracepoint を見られる。

7.4 raw_tracepoint

tracepoint のより高速な版。tracepoint 引数をデコードせずそのまま受け取る — 若干のコード記述負担はあるが、より速い。

7.5 uprobe / uretprobe

ユーザー空間関数にも付けられる。例: libc の malloc に attach してすべての呼び出しを追跡。

SEC("uprobe//usr/lib/libc.so.6:malloc")
int on_malloc(struct pt_regs *ctx) {
    size_t size = PT_REGS_PARM1(ctx);
    /* ... */
    return 0;
}

非常に強力だが高価だ — 呼び出しごとにユーザー → カーネルトラップが起こる。

7.6 perf_event

perf イベント (CPU サイクル、キャッシュミスなど) に attach。CPU プロファイリングの土台。

SEC("perf_event")
int sample(struct bpf_perf_event_data *ctx) {
    /* N サイクルごとに呼ばれる */
    return 0;
}

7.7 XDP — eXpress Data Path

ネットワークインターフェースの受信経路の最前に attach。パケットが sk_buff に変換される前、ドライバ直後に呼ばれる。非常に速い。

SEC("xdp")
int xdp_prog(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    if (data + sizeof(struct ethhdr) > data_end)
        return XDP_PASS;

    struct ethhdr *eth = data;
    if (eth->h_proto == bpf_htons(ETH_P_IP)) {
        /* IP packet */
    }
    return XDP_PASS;  // または XDP_DROP, XDP_TX, XDP_REDIRECT
}

XDP アクション:

  • XDP_PASS: 通常のネットワークスタックへ
  • XDP_DROP: ドロップ
  • XDP_TX: 同じ NIC へ再送信
  • XDP_REDIRECT: 別の NIC へ送信
  • XDP_ABORTED: エラー

XDP は DDoS 保護、ロードバランシング、パケット書き換えで非常に人気。Cloudflare が自身のインフラに XDP を広範に使っている。

7.8 tc (Traffic Control)

XDP より少し後の段階で attach。sk_buff が既に作られているので、より豊かな情報を持つ (cgroup、socket など)。XDP ほど速くはないがより柔軟。

tc-bpf で attach:

tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj my_prog.bpf.o

7.9 cgroup hooks

cgroup 内のすべてのプロセスの特定のシステムコールに attach 可能。例えばある cgroup のすべての connect 呼び出しを阻止するなど:

SEC("cgroup/connect4")
int restrict_connect(struct bpf_sock_addr *ctx) {
    if (ctx->user_port == bpf_htons(22)) {
        return 0;  // SSH ブロック
    }
    return 1;
}

コンテナセキュリティポリシーの土台だ。

7.10 LSM hooks (KRSI)

5.7 で導入。Linux Security Module のすべてのフックに BPF プログラムを差し込める。SELinux/AppArmor を BPF で置き換えたり補強したりできる。

SEC("lsm/file_open")
int BPF_PROG(check_file_open, struct file *file, int ret) {
    /* ファイルオープンを検査し拒否できる */
    return -EPERM;  // または 0 = OK
}

Tetragon はこのモデルの上で動く。

7.11 sched_ext (6.12+)

最も新しい attach point。ユーザー空間が BPF でスケジューリングポリシーを記述できる。Linux スケジューラ記事で詳しく扱った。

7.12 socket lookup、sock_ops、sk_msg

socket 処理の様々な段階に attach できる。Cilium の sidecar なしのサービスメッシュがこれを活用する。


8. CO-RE — Compile Once, Run Everywhere

8.1 問題

eBPF プログラムはしばしばカーネルデータ構造 (task_structsk_buff など) を読む。しかしこれらの構造体のレイアウトはカーネルバージョン、コンパイルオプションごとに異なる。ビルドしたマシンと実行するマシンが違うと壊れる。

昔の BCC はこの問題を「実行時にコンパイル」で解決した。ユーザーマシンに clang とカーネルヘッダをすべてインストールし、実行のたびにコンパイルした。遅くて、ディスクを大量に消費し、運用に不向き。

8.2 BTF — BPF Type Format

BTF はカーネルデータ構造のメタデータを埋め込む軽量なデバッグ情報フォーマット。DWARF の簡略化版。5.2 からカーネルが自分の BTF を /sys/kernel/btf/vmlinux に公開する。

BTF はすべてのカーネル構造体のフィールド名とオフセットを含む。ユーザー空間ツールがこれを読めば「このカーネルで task_struct->mm はどこにあるか」が分かる。

8.3 CO-RE の動作

CO-RE は BTF を基盤にして BPF プログラムが別のカーネルバージョンでも動くようにする。

#include <vmlinux.h>          // ホストカーネルの BTF から生成したヘッダ
#include <bpf/bpf_core_read.h>

SEC("kprobe/sys_open")
int hello(struct pt_regs *ctx) {
    struct task_struct *task = (void *)bpf_get_current_task();
    pid_t pid = BPF_CORE_READ(task, pid);  // マクロの魔法
    /* ... */
    return 0;
}

BPF_CORE_READ マクロはコンパイル時に「このフィールドのオフセット」を直接インラインしない。代わりに「フィールド位置を BTF で lookup せよ」という再配置 (relocation) 情報を ELF に残す。

実行時に libbpf がその再配置を処理する — ホストカーネルの BTF を見て実際のオフセットを埋める。同じ BPF ELF が 5.10 カーネルと 6.5 カーネルの両方で動く。

8.4 vmlinux.h の生成

bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

このファイルは約 4MB で、すべてのカーネル構造体を定義している。BPF コードはこれを include する。

8.5 Field existence 検査

CO-RE のもう一つの機能: フィールドが存在するか検査する。フィールドが新しく追加・削除されたときに柔軟に対応できる。

if (bpf_core_field_exists(task->cgroups)) {
    /* このフィールドがあるカーネル */
} else {
    /* ないカーネル */
}

8.6 影響

CO-RE のおかげで BPF ツールは本当に portable になった。Falco、Cilium、Tetragon、Pixie のようなモダンツールはすべて CO-RE を使う。一度ビルドしたバイナリがどのカーネルでも動く (BTF があるという前提の下で)。


9. ユーザー空間ツール — libbpf、BCC、bpftrace

9.1 BCC — 古い流儀

BCC (BPF Compiler Collection) は最も古い BPF ツールセット。Python/Lua ラッパーで BPF プログラムを簡単に書けるようにする。

from bcc import BPF

prog = """
int hello(void *ctx) {
    bpf_trace_printk("Hello!\\n");
    return 0;
}
"""

b = BPF(text=prog)
b.attach_kprobe(event="sys_open", fn_name="hello")
b.trace_print()

問題: 実行のたびにコンパイル。clang + カーネルヘッダが必要。大きなディスク領域。運用環境に不向き。

BCC は今も多くのツールで使われ、サンプル倉庫としての価値も大きい (/usr/share/bcc/tools/ に 200 を超えるツールがある)。

9.2 libbpf — モダンな流儀

libbpf は C ライブラリで、BPF プログラムのロード・attach をカプセル化する。CO-RE をサポートする。

#include <bpf/libbpf.h>

int main() {
    struct bpf_object *obj = bpf_object__open_file("hello.bpf.o", NULL);
    bpf_object__load(obj);
    struct bpf_program *prog = bpf_object__find_program_by_name(obj, "hello");
    bpf_program__attach(prog);

    while (1) sleep(1);
    return 0;
}

ビルドして一度配れば済む。Datadog、Cilium、Tetragon がすべて libbpf ベース。

9.3 bpftrace — DSL

最速の入門ツール。awk に似た DSL で BPF プログラムを一行で書く。

# sys_open 呼び出しごとのカウント
bpftrace -e 'kprobe:sys_open { @[comm] = count(); }'

# vfs_read のレイテンシ分布 (ヒストグラム)
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
    @lat = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# スタックトレース付きのページフォルト追跡
bpftrace -e 'tracepoint:exceptions:page_fault_user { @[ustack] = count(); }'

bpftrace は内部で libbpf を使う。DSL を BPF C コードに変換し、clang でコンパイルし、libbpf でロードする。

9.4 どのツールを使うか

  • その場での診断: bpftrace
  • 商用ツール・長時間動かすエージェント: libbpf (C/Rust/Go)
  • 参考用・既存の BCC ツール活用: BCC

新しい BPF コードのほとんどは libbpf に移行している。BCC は次第に「サンプル倉庫」の役割に限定されてきている。


10. 事例 1 — bpftrace 一行診断

bpftrace の力を見せる実戦例:

10.1 どのプロセスがディスクを最も読むか

bpftrace -e '
tracepoint:block:block_rq_issue { @[comm] = sum(args->bytes); }
'

10.2 誰がシステムコールを最も呼ぶか

bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

10.3 TCP 再送の追跡

bpftrace -e '
kprobe:tcp_retransmit_skb {
    @[comm] = count();
}
'

10.4 どの関数が最も時間がかかるか

bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
    $duration = nsecs - @start[tid];
    @hist = hist($duration / 1000);
    delete(@start[tid]);
}'

10.5 OOM kill 時の全コンテキスト

bpftrace -e '
kprobe:oom_kill_process {
    printf("OOM kill: comm=%s pid=%d ustack=%s\n",
        comm, pid, ustack);
}'

各一行が、精巧なツールがするはずの仕事をする。運用デバッグの新しい基準点だ。


11. 事例 2 — XDP DDoS 防御

11.1 シナリオ

UDP フラッド攻撃を受けている。毎秒数億のパケットが入ってきて NIC が麻痺している。防御コードはパケットが sk_buff に変換される前に動かなければならない。

11.2 XDP プログラム

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __type(key, struct bpf_lpm_trie_key);
    __type(value, __u32);
    __uint(max_entries, 1024);
    __uint(map_flags, BPF_F_NO_PREALLOC);
} blacklist SEC(".maps");

SEC("xdp")
int xdp_drop(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end) return XDP_PASS;
    if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end) return XDP_PASS;

    /* IP を LPM trie で lookup */
    struct {
        __u32 prefixlen;
        __u32 addr;
    } key = { .prefixlen = 32, .addr = ip->saddr };

    if (bpf_map_lookup_elem(&blacklist, &key)) {
        return XDP_DROP;  // ブラックリストなら即ドロップ
    }

    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

11.3 Attach

ip link set dev eth0 xdpgeneric obj xdp_drop.bpf.o sec xdp

11.4 結果

このプログラムはパケットが sk_buff に変換される前にドロップする。約 10 倍速い (24Mpps vs 2.5Mpps のような数値)。Cloudflare が自身のインフラで非常に似たパターンを使っている。

XDP がドロップしたパケットはシステム metric にほぼ影響を与えない — sk_buff alloc もしないのでメモリも使わず、CPU もほとんど使わない。


12. 事例 3 — Cilium の Kubernetes ネットワーキング

12.1 ビジョン

Cilium は Kubernetes のネットワーキング・セキュリティ・可観測性を eBPF で書き直すプロジェクト。iptables ベースの kube-proxy を完全に置き換える。

12.2 何が違うか

伝統的な Kubernetes ネットワーキング:

  • iptables ルール数万 (サービス数に比例)
  • 新しいサービスごとにすべてのノードで iptables 更新
  • パケット処理は conntrack、NAT、ルーティングをすべて通る
  • 大きなクラスタでは本当に遅い

Cilium のモデル:

  • BPF maps にサービス・エンドポイント情報を保存
  • パケット処理は BPF プログラムが直接行う
  • iptables はほぼ不要
  • 大きなクラスタでも一貫したパフォーマンス

12.3 sidecar なしのサービスメッシュ

Cilium は sock_ops と sk_msg を活用して sidecar プロキシなしで L7 通信を横取りできる。伝統的な Istio/Linkerd モデルは pod ごとに Envoy を sidecar として立てるが、これはメモリ・CPU・レイテンシのコストが大きい。

Cilium の sidecarless モデルはノードごとに 1 つのプロキシ (または 0 個) だけを立て、BPF でトラフィックをそのプロキシへリダイレクトする。

12.4 Tetragon — セキュリティ

Cilium チームが作ったもう一つのツール。LSM フックと tracepoint を活用してすべてのコンテナ活動を監視する。「このコンテナが /etc/passwd を読んだ」をリアルタイムで知ることができる。

伝統的な Falco に似ているがより深い。ポリシー違反時にシグナルを送ったり即座にブロックしたりできる。

★ Insight ─────────────────────────────────────

  • eBPF が生んだ新しい会社: Isovalent (Cilium の会社、2024 年に Cisco が買収)、Polar Signals (continuous profiling)、Pixie (可観測性)、Groundcover、Levitate Security。すべて eBPF が可能にした新カテゴリだ。
  • iptables 時代の終わり: Kubernetes インフラで iptables は今やレガシー扱いだ。nftables が一部代替したが、本当の未来は eBPF。Cilium が事実上の標準になりつつある。
  • sidecar なしメッシュの意味: Kubernetes クラスタで sidecar プロキシはしばしばノードメモリの 30% 以上を占める。eBPF で置き換えるとそのメモリが解放される。これはコスト面で非常に大きな差だ。 ─────────────────────────────────────────────────

13. 事例 4 — Falco ランタイムセキュリティ

Falco は sysdig が作ったランタイムセキュリティツール。コンテナの異常動作を検知してアラートを送る。例: /etc/shadow を読むコンテナ、root への昇格試行、suspicious shell spawn など。

伝統的には sysdig カーネルモジュールを使っていたが、最新版は eBPF に移行した。すべてのシステムコールを横取りしてルールエンジンに送る。

# Falco ルールの例
- rule: Read sensitive file
  desc: An attempt to read sensitive file
  condition: open_read and sensitive_files
  output: Sensitive file opened (user=%user.name file=%fd.name)
  priority: WARNING

eBPF のおかげでカーネルモジュールが不要になり、どのカーネルバージョンでも動く。


14. 事例 5 — bpftune 自動チューニング

Oracle が作ったツール。eBPF でシステムメトリクスを監視し、自動的に sysctl 値を調整する。例:

  • TCP 接続がよくタイムアウト → tcp_keepalive_time を下げる
  • メモリ圧迫がよく発生 → vm.swappiness を調整
  • ディスク IO ボトルネック → readahead サイズを増やす

伝統的にシステムチューニングは人手で行われた。bpftune はこれをデータ駆動で自動化する。eBPF がなければメトリクスごとに別々のツールを立てなければならなかっただろう。


15. セキュリティ — eBPF の危険

eBPF は強力な分、間違った手に渡ると危険だ。

15.1 BPF 権限

BPF プログラムのロードには通常 CAP_BPF (5.8+) と CAP_PERF_MON または CAP_NET_ADMIN が必要。以前は CAP_SYS_ADMIN (root 権限とほぼ同等) が必要だった。

15.2 Verifier 回避

verifier は静的解析であり、100% 完璧ではない。過去に数回の CVE が verifier を騙して任意メモリの読み書きを可能にした:

  • CVE-2022-23222: BPF pointer arithmetic 検証の欠陥
  • CVE-2021-45402: 32 ビット分岐検証の欠陥
  • CVE-2021-3490: ALU32 boundary tracking の欠陥

これらの欠陥は発見されるたびに素早く修正されるが、verifier が次第に複雑になるにつれて新しい欠陥の可能性も増える。

15.3 unprivileged_bpf_disabled

Linux は一般ユーザーの BPF 使用をデフォルトで阻止できる:

echo 1 > /proc/sys/kernel/unprivileged_bpf_disabled

ほとんどのディストリビューションがこれをデフォルトで有効にしている。一般ユーザーは BPF プログラムをロードできない。

15.4 BPF LSM と BPF 自身の保護

BPF LSM があるということは BPF で BPF を制御できるという意味だ。「この cgroup の BPF プログラムロードは拒否する」のようなポリシーを BPF で表現できる。

15.5 サイドチャネル

BPF プログラムは何らかの権限情報を漏らす可能性がある。Spectre のようなサイドチャネル攻撃が BPF で可能だという研究がある。verifier は一部の懸念あるパターンを拒否するが、完璧な防御は難しい。


16. デバッグ — bpftool

bpftool は BPF インフラの万能ツールだ。

16.1 ロード済みプログラムを見る

bpftool prog list
1: kprobe  name hello  tag a1b2c3d4e5f60718
        loaded_at 2026-04-15T10:30:00+0900  uid 0
        xlated 200B  jited 256B  memlock 4096B
        btf_id 5

16.2 プログラムの BPF コードをダンプ

bpftool prog dump xlated id 1

検証を通過した BPF 命令を見せる。

bpftool prog dump jited id 1

JIT 済みネイティブコードを見せる。

16.3 map を見る

bpftool map list
bpftool map dump id 5

16.4 BTF ダンプ

bpftool btf dump file /sys/kernel/btf/vmlinux | less

16.5 Verifier ログ

プログラムロード時に詳細な verifier ログを見たければ:

bpftool prog load my.bpf.o /sys/fs/bpf/my_prog --log_level 7

17. 未来 — eBPF の次の一歩

17.1 sched_ext

Linux スケジューラ記事で扱った。ユーザー空間が BPF でスケジューリングポリシーを書けるようにする。6.12 でメインラインマージ。

17.2 struct_ops

BPF プログラムがカーネルインターフェースの実装になれるようにする。例えば bpf_struct_ops メカニズムで TCP congestion control アルゴリズムを BPF で実装できる。

17.3 BPF for filesystem operations

ファイルシステムフックに BPF を attach できるようにする作業が進行中。ユーザー空間定義のファイルシステムポリシー (例: キャッシュポリシー、配置ポリシー) が可能になる。

17.4 BPF in eBPF

自分自身を呼ぶ BPF? 一部の抽象化作業で可能。tail call の一般化。

17.5 他 OS への拡大

  • ebpf-for-windows: Microsoft が後援。Windows カーネルに eBPF インフラを持ち込もうとする試み。
  • uBPF: ユーザー空間で BPF VM を動かすライブラリ。AWS Firecracker が使用。

eBPF が OS の境界を越える標準になる可能性がある。


18. 結論 — eBPF は終わらない

ここまで読んだなら、次の質問に答えられるはずだ:

  • eBPF とは何か、cBPF と何が違うか?
  • verifier はどう安全性を保証するか?
  • BPF Map はどうユーザー空間と通信するか?
  • どんな attach point があるか?
  • CO-RE は何を解くか?
  • libbpf、BCC、bpftrace の違いは?
  • XDP はどう速いか?
  • Cilium は何を書き直したか?

しかしこの記事は始まりに過ぎない。eBPF は毎年新機能が加わり、毎年新領域に拡大する。1 年後の eBPF は今日とは大きく違うだろう。

eBPF を学ぶ最良の方法は自分でやってみることだ:

  1. bpftrace 一行コマンドでシステム診断を始める
  2. BCC の /usr/share/bcc/tools/ のツールを読んで修正する
  3. libbpf サンプルで自分のツールを書く
  4. verifier のエラーに出会ったら一行ずつ読む

このステップを経ると eBPF はもはや魔法ではなく、強力だが理解可能なツールに見えるようになる。

この記事で Linux 内部構造シリーズとの姉妹作も締めくくる。シリーズが「カーネルが何をするか」を扱ったなら、この記事は「ユーザーがカーネルをどう安全に拡張するか」を扱った。二つが揃うとモダン Linux システムの基本精神が描かれる。

次の記事では [Cilium 内部構造ディープダイブ] または [BPF で作られた新カテゴリツール] を扱う予定だ。


付録 A — 参考資料

付録 B — よくある質問

Q: eBPF を学ぶにはどう始めればよいか? A: bpftrace から始める。一行コマンドを真似ていると自然に BPF モデルが身につく。その後 BCC ツールのコードを読み、最後に libbpf-bootstrap で自分のツールを書く。

Q: eBPF プログラムがカーネルを壊せるか? A: verifier が誤って通した欠陥があるなら可能。しかしこれは非常に稀で素早くパッチされる。通常の使用では BPF がカーネルクラッシュを起こさない。

Q: kprobe と tracepoint、どちらを使うべきか? A: tracepoint があれば tracepoint。安定で速い。tracepoint がない所では kprobe (または fentry)。

Q: BCC と libbpf、どちらを使うか? A: 新しいコードは無条件 libbpf。BCC は古いツールの保守か学習用。

Q: XDP と tc のどちらが速いか? A: XDP。パケットが sk_buff に変換される前に処理する。tc は sk_buff 以降なので若干遅いがより豊かな情報を持つ。

Q: Cilium は本当に iptables を完全に置き換えるか? A: ほぼそう。Cilium モードでは kube-proxy の iptables ルールを作らない。ただしホストの一部の基本ルールは依然残ることがある。

Q: eBPF と DTrace の関係は? A: DTrace は Solaris の動的トレースインフラ。似たモデルだがより早く作られ、別の道を行った。eBPF は cBPF から出発し、より汎用的だ。今日の eBPF は DTrace がやっていたことのほとんどすべてをできる。

Q: eBPF は Java/Go の GC pause を追跡できるか? A: 可能。uprobe で GC のエントリ・リターン関数に attach すれば GC 時間を測定できる。JVM Flight Recorder の代替として使える。


付録 C — ミニ用語集

  • BPF: Berkeley Packet Filter。1992 年 BSD で開始。
  • cBPF: classic BPF。古いパケットフィルタ ISA。
  • eBPF: extended BPF。2014+ のモダン BPF。
  • Verifier: BPF プログラムの安全性を静的検証するモジュール。
  • JIT: Just-In-Time コンパイラ。BPF バイトコードをネイティブコードに変換。
  • BTF: BPF Type Format。カーネルデータ構造のメタデータ。
  • CO-RE: Compile Once, Run Everywhere。BTF ベースの portable BPF。
  • libbpf: BPF プログラムのロード・attach ライブラリ。
  • BCC: BPF Compiler Collection。古い BPF ツールセット。
  • bpftrace: BPF 用 awk ライクな DSL。
  • bpftool: BPF インフラデバッグツール。
  • kprobe: カーネル関数のエントリポイントにフック。
  • kretprobe: カーネル関数のリターンポイントにフック。
  • fentry/fexit: ftrace trampoline ベースの高速 kprobe 代替。
  • tracepoint: カーネルに埋め込まれた安定したトレース地点。
  • uprobe: ユーザー空間関数にフック。
  • XDP: eXpress Data Path。NIC ドライバ直後の BPF フック。
  • tc: Traffic Control。sk_buff 以降の BPF フック。
  • LSM: Linux Security Module。セキュリティフック。
  • KRSI: Kernel Runtime Security Instrumentation。BPF LSM の別名。
  • sched_ext: BPF で書くスケジューラ (6.12+)。
  • struct_ops: BPF がカーネルインターフェースの実装になるメカニズム。
  • PERCPU map: CPU ごとに分離された map。ロックなし。
  • RINGBUF: 新しい BPF event ring buffer (5.8+)。
  • BPF tail call: BPF プログラムが別の BPF プログラムへジャンプ。
  • Cilium: BPF で書き直した Kubernetes ネットワーキング・セキュリティ。
  • Tetragon: BPF ベースのランタイムセキュリティ (Cilium チーム)。
  • Falco: BPF ベースのランタイムセキュリティ (Sysdig)。
  • bpftune: BPF による自動システムチューニング (Oracle)。

この記事は Linux 内部構造シリーズと姉妹作だ。シリーズはカーネルがユーザーにしてくれることを扱った。この記事はユーザーが安全にカーネル内に入る方法を扱った。二つの風景が揃うとモダン Linux の精神が描かれる。