このページの目次

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 (x < 10) {
    // verifier はこの分岐内で x ∈ [0, 9] を導出
    bpf_map_lookup_elem(&map, &x);   // OK: インデックスが境界内であることが証明済み
}
// 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 — 一般的なタイプの抜粋
enum bpf_map_type {
    BPF_MAP_TYPE_HASH,          // 汎用ハッシュテーブル (最も一般的)
    BPF_MAP_TYPE_ARRAY,         // 固定サイズ配列 (O(1) 検索、削除不可)
    BPF_MAP_TYPE_PERCPU_HASH,   // per-CPU ごとに個別に保持、ロックフリー
    BPF_MAP_TYPE_LRU_HASH,      // 満杯時に LRU で淘汰
    BPF_MAP_TYPE_RINGBUF,       // リングバッファ、イベントストリームをユーザー空間へ
    BPF_MAP_TYPE_STACK_TRACE,   // カーネルスタックトレース格納
    BPF_MAP_TYPE_BLOOM_FILTER,  // 「集合内に存在する可能性」の高速チェック
};

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つの部分からなります:

  1. BTF (BPF Type Format)——カーネルに組み込まれた型記述で、/sys/kernel/btf/vmlinux に埋め込まれており、数MBに圧縮された「カーネルデバッグ情報」に相当します。
  2. libbpf の CO-RE リロケーション⁠——.o をロードする際に現在のカーネルの BTF を読み取り、バイトコード内のすべてのフィールドオフセットをその場で修正して、ローカルの実際のオフセットに合わせます。

これにより、同じ .o ファイルを 5.10 と 6.5 の両方で、修正を加えずに実行できます:

#include <bpf/bpf_core_read.h>
x = BPF_CORE_READ(task, pid);   // オフセットはロード時にローカルカーネルに基づいて解析

bpftool: BPF オブジェクトの管理

bpftool prog list                          # ロード済みのプログラムを一覧表示
bpftool map dump id <map_id>               # map の内容をエクスポート
bpftool prog dump xlated id <prog_id>      # BPF バイトコードを表示
bpftool prog dump jited  id <prog_id>      # JIT 後の x86 アセンブリを表示
bpftool prog load myprog.o /sys/fs/bpf/myprog   # ロードして bpffs にピン留め

セキュリティモデル

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