このページの目次
eBPF 基礎
eBPFはユーザー空間からサンドボックス化されたプログラムをカーネルに注入して実行することを可能にします——verifierが安全性を静的に証明し、JITでネイティブ命令にコンパイルされ、mapsを通じてユーザー空間とデータを共有し、CO-REにより「1回コンパイル、どこでも実行」を実現します。カーネルにおけるeBPFは、ブラウザにおけるJavaScriptに近似します。
概要
カーネル内に独自のロジックを追加したい場合、従来は2つの選択肢しかありませんでした:カーネルソースコードを修正して再コンパイルする(サイクルは月単位)、またはカーネルモジュールを書く(1つのNULLポインタでシステム全体がパニックします)。eBPF (extended Berkeley Packet Filter) は3つ目の道を提供します:プログラムをデータとしてカーネルに提出し、カーネルはまず静的検証器によって無害であることを証明し、その後JITでネイティブコードに変換してフックに接続して実行するというものです。ブラウザが任意のウェブページのJavaScriptを実行できるのと同様に、カーネルもユーザーが提出したコードを実行できるようになりました——これが、eBPFが当初のパケットフィルタ (cBPF) から汎用のカーネルプログラム可能プラットフォームへと進化してきた理由です。
バージョン範囲は 3.18 ~ 6.x です。現在、eBPFは以下の4つの主要な分野を支えています:
- 可観測性: bpftrace, bcc, Cilium Hubble
- ネットワーク: XDP, TC BPF, Cilium コンテナネットワーク
- セキュリティ: BPF LSM, seccomp
- パフォーマンス: カーネル再コンパイル不要のホットパス上のカスタムロジック
ライフサイクル
flowchart LR
subgraph user["👩💻 ユーザー空間"]
C["C コード"] -->|"clang -target bpf -O2"| O["eBPF ELF (.o)"]
O -->|"libbpf + CO-RE リロケーション"| L["bpf(BPF_PROG_LOAD)"]
R["maps / ringbuf の読み取り"]
end
subgraph kernel["🛡️ カーネル空間"]
L --> V{"<b>Verifier</b><br><small>全パスの安全性を静的に証明</small>"}
V ==>|"通過"| J["<b>JIT コンパイル</b><br><small>バイトコード → ネイティブ命令</small>"]
V -.->|"拒否 -EACCES<br>+ verifier ログ"| C
J --> A["フックへのアタッチ<br><small>kprobe / tracepoint / XDP…</small>"]
A -->|"イベントトリガーで実行"| M[("BPF maps")]
end
M --> R
classDef gate fill:#d2992226,stroke:#d29922,stroke-width:2.5px
classDef fast fill:#4493f81f,stroke:#4493f8,stroke-width:2px
classDef store fill:#3fb9501f,stroke:#3fb950,stroke-width:2px
class V gate
class J,A fast
class M store
鍵となるのは順序です:検証 → コンパイル → アタッチ。verifier がすべての実行パスで安全であることを証明しない限り、プログラムはフックに接続されることはありません。
Verifier: 安全性の基盤
kernel/bpf/verifier.c、約1.6万行、カーネル内で最も複雑な検証器です。プログラムに対してシンボル実行を行い、考えられるすべてのパスを列挙して、以下の3つの性質をチェックします:
| カテゴリ | 具体的な制約 |
|---|---|
| コントロールフロー | 到達不能な命令の存在なし;無限ループなし(5.3+ では終了が証明された有界ループを許可);境界外ジャンプなし;呼び出し深度 ≤ 32 |
| データフロー | 各レジスタの型を追跡(PTR_TO_CTX / PTR_TO_MAP_VALUE / SCALAR_VALUE…);各スカラーの値の範囲を追跡;NULLポインタチェックの強制;map アクセスにおける key/value のサイズ一致 |
| リソース | 命令数の上限 100 万(5.1+);スタック深度 512 バイト |
検証に失敗した場合、BPF_PROG_LOAD は -EACCES を返し、命令ごとの verifier ログが添付されます——このログを読み解くことは、eBPF 開発における日常業務です。
バウンダートラッキング (境界追跡)
verifier の最も精巧なメカニズムは、各レジスタに対して [smin, smax, umin, umax] の4つの境界を維持し、分岐に沿って条件を「学習」させることです:
if
// else パスでは x ∈ [10, MAX] が導出される
配列インデックスの範囲外アクセスの防止、map key の正当性、ポインタ算術の範囲外アクセスの防止——これらはすべて、この区間導出によってロード時に静的に証明され、ランタイムではゼロオーバーヘッドで動作します。
JIT コンパイル
検証が通過すると、BPF バイトコードは命令ごとにネイティブマシンコード(x86 の場合 arch/x86/net/bpf_jit_comp.c)に変換されます。各 BPF 命令は 1~3 命令の x86 命令に対応します:
BPF_ADD reg, imm → add $imm, %reg
BPF_LDX reg, src → mov (%src), %reg
BPF_CALL fn → call fn (tail call は jmp に最適化)
BPF_EXIT → ret
つまり、eBPF は「カーネル内のインタプリタ」ではありません——JIT 後の実行パフォーマンスは手書きのカーネルコードに近く、これが eBPF が XDP のような毎パケット通過のホットパスに配置される自信の源です。
BPF Maps: プログラム間でのデータ共有
BPF プログラム自体はステートレスでイベントドリブンです。ステートは maps に格納されます——カーネル空間のキーバリューストアであり、BPF プログラム間、および BPF とユーザー空間の間でデータを共有するために使用されます。
// include/uapi/linux/bpf.h — 主要なタイプの抜粋
;
BPF 側ではヘルパー関数 bpf_map_lookup_elem / bpf_map_update_elem / bpf_map_delete_elem を介して操作します。ユーザー空間では bpf() システムコールまたは libbpf のラッパーを介して、同じ map の読み書きを行います。
Ring Buffer と Perf Buffer
カーネルイベントをユーザー空間へ継続的に送信することは、可観測性シナリオの主要な経路です。5.8 より前は perf buffer を使用していましたが、それ以降は ringbuf を一律で使用するべきです:
| Perf Buffer(旧) | Ring Buffer (5.8+) | |
|---|---|---|
| バッファ構造 | CPU ごとに1つ | 全 CPU で共有1つ |
| 順序 | CPU 間で順序が乱れる | グローバルに順序付けられる |
| メモリ効率 | 最もアクティブな CPU に合わせて予約 | 共有によりメモリを節約 |
| コピー | 2回 | 予約-コミット (reserve/commit)、mmap によるゼロコピー |
| レコード長 | 固定 | 可変 |
CO-RE: Compile Once, Run Everywhere
問題:BPF プログラムはコンパイル時にカーネル構造体を参照し、フィールドのオフセットがバイトコードにハードコードされます。次のカーネルバージョンでフィールドの位置が変更されると、プログラムは間違ったメモリを読み取ってしまいます。かつての bcc は、ターゲットマシン上でその場でコンパイルする必要がありました(LLVM とカーネルヘッダーの一式が必要)。
解決策は2つの部分から成ります:
- BTF (BPF Type Format)——カーネルに組み込まれた型記述で、
/sys/kernel/btf/vmlinuxに格納されており、数 MB に圧縮された「カーネルデバッグ情報」に相当します。 - libbpf の CO-RE リロケーション——
.oをロードする際に現在のカーネルの BTF を読み取り、バイトコード内のすべてのフィールドオフセットをその場で修正して、ローカルカーネルの実際のオフセットに合わせます。
これにより、同じ .o ファイルを 5.10 と 6.5 の両方で変更なしで実行できます:
x = ; // オフセットはロード時にローカルカーネルに基づいて解析される
bpftool: BPF オブジェクトの管理
セキュリティモデル
eBPF プログラムは「カーネルに害を及ぼすことが不可能な」サブセットに制約されており、以下を行うことができません:
- 無限に実行する(命令数の上限 + 終了性の検証)
- バウンダリチェックのないポインタアクセスを行う
- 任意のカーネルメモリを直接読み取る——ヘルパー(
bpf_probe_read_kernel)を経由する必要がある - 任意のカーネルメモリを直接書き込む——自分の maps のみに書き込める
- 睡眠する、または
scheduleを呼び出す(sleepable BPF は制限付きの例外) - 白リスト(ヘルパー/kfunc)以外のカーネル関数を呼び出す
特権レベル:
| 能力 | 許可される操作 |
|---|---|
CAP_BPF | ほとんどの BPF プログラムタイプのロード |
CAP_SYS_ADMIN | すべてのタイプ、トレーシングを含む |
| 無特権 | kernel.unprivileged_bpf_disabled=0 の場合のみ、ソケットフィルタなどの限られたタイプのみ |
プログラムがどのフックにアタッチできるか、各タイプで利用可能なコンテキストについては、eBPF プログラムタイプ を参照してください。実践的なトレースツールチェーンについては、トレーシングとデバッグ および ftrace と bpftrace ユーザー空間 を参照してください。
参考
- ソースコード:
kernel/bpf/(verifier, syscall, helpers),arch/x86/net/bpf_jit_comp.c(JIT),tools/lib/bpf/(libbpf),tools/bpf/bpftool/ - カーネルドキュメント:
Documentation/bpf/(非常に優れたドキュメント) - 書籍: "BPF Performance Tools" (Brendan Gregg)
- LWN: "A thorough introduction to eBPF" (lwn.net/Articles/740157/)
キーワード: eBPF, verifier, JIT, BPF maps, BTF, CO-RE, libbpf, bpftool, ring buffer, CAP_BPF