このページの目次
eBPF 基礎
eBPFはユーザー空間からサンドボックス化されたプログラムをカーネルに注入して実行することを可能にします。verifierが安全性を静的に証明し、JITでネイティブ命令にコンパイルし、mapsを通じてユーザー空間とデータを共有し、CO-REにより「1回コンパイル、どこでも実行」を実現します。カーネルにおけるeBPFの位置づけは、ブラウザにおけるJavaScriptに近似します。
概要
カーネル内に独自のロジックを追加したい場合、従来は2つの選択肢しかありませんでした:カーネルソースコードを修正して再ビルド(サイクルは月単位)、またはカーネルモジュールを書く(1つのNULLポインタでシステム全体がパニックします)。eBPF (extended Berkeley Packet Filter) は3つ目の道を提供します:プログラムをデータとしてカーネルに提出し、カーネルはまず静的検証器によって無害であることを証明し、その後JITでネイティブコードに変換してフックに接続して実行するというものです。ブラウザが任意のウェブページのJavaScriptを実行できるのと同様に、カーネルもユーザーが提出したコードを実行できるようになりました——これが、当初のパケットフィルタ (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 開発における日常業務です。
境界追跡 (Bound Tracking)
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 後の実行パフォーマンスは手書きのカーネルコードに近く、これが毎パケット通過するホットパスである 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 を使用していましたが、5.8 以降は 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