Skip to content
Published on

eBPF基礎完全制覇 — プログラム、マップ、そしてVerifierの世界

Authors

はじめに

運用中のサーバーで「特定のプロセスがどのファイルを開いているかをリアルタイムで見たい」という要件が発生したとしましょう。従来の方法は二つでした。カーネルモジュールを自作してロードするか、straceのようなptraceベースのツールでプロセスを捕まえて観察するかです。前者はカーネルパニックのリスクを受け入れる必要があり、後者は対象プロセスを数十倍遅くしてしまいます。本番環境ではどちらも簡単には選べません。

eBPF(extended Berkeley Packet Filter)はこのジレンマを根本から解決しました。カーネルの再コンパイルもモジュールのロードもせずに、検証済みの安全なプログラムをカーネル内部のほぼあらゆるポイントにアタッチして実行できます。今日では、CiliumのようなCNI、Katranのようなロードバランサー、FalcoやTetragonのようなセキュリティツール、そして数多くの可観測性ツールがすべてeBPFの上で動いています。

この記事では、eBPFの中核要素であるプログラム、マップ、Verifierを順に見ていき、libbpfとCO-REを使った初めてのプログラムを最初から最後まで書いてみます。可観測性編とセキュリティ編へと続くシリーズの第1回です。

eBPFがカーネルを変えた方法

カーネルモジュールの時代とその限界

Linuxカーネルの動作を拡張する伝統的な方法はカーネルモジュール(LKM)でした。モジュールは強力ですが、致命的な欠点があります。

項目カーネルモジュール (LKM)eBPFプログラム
安全性バグ一つでカーネルパニックの恐れVerifierがロード前に安全性を検証
分離カーネル全メモリにアクセス可能ヘルパー関数経由の制限付きアクセスのみ
互換性カーネルバージョンごとに再コンパイルCO-REで単一バイナリが複数カーネルで動作
デプロイモジュール署名やポリシーの問題システムコール一つでロード、権限で統制
終了保証無限ループが可能プログラムの終了が静的に証明されないとロード不可
学習コストカーネル内部API全般の理解が必要制限されたヘルパーAPIとCのサブセット

eBPFの核心アイデアは「カーネル内で動かすコードを、カーネルが事前に検証する」ことです。JavaScriptがブラウザというサンドボックスの中でWebをプログラマブルにしたように、eBPFはカーネルをプログラマブルにしました。eBPFが「カーネルのJavaScript」と例えられるのには理由があります。

cBPFからeBPFへ

元々のBPFは、1992年にtcpdumpのパケットフィルタリングのために設計された小さな仮想マシン(classic BPF、cBPF)でした。2014年のカーネル3.18で拡張版のeBPFが導入され、次の点が変わりました。

  • レジスタが2個から11個(R0〜R10)に増え、64ビットに拡張
  • マップという永続的データ構造の導入により、カーネルとユーザー空間のデータ共有が可能に
  • ヘルパー関数呼び出しのサポートにより、カーネル機能へ安全にアクセス
  • JITコンパイラによりネイティブコード並みの性能を確保
  • ネットワーキングを超えて、トレーシングやセキュリティなど汎用実行エンジンへ拡張

アーキテクチャ全体を一望する

eBPFプログラムが書かれてからカーネル内で実行されるまでの道のりを図にすると次のようになります。

ユーザー空間(User space)
+--------------------------------------------------------------+
|  Cソース (.bpf.c)                                             |
|     |  clang -target bpf -g (BTF付き)                        |
|     v                                                        |
|  ELFオブジェクト (.bpf.o)                                     |
|     |                                                        |
|     v                                                        |
|  ローダー (libbpf / cilium-ebpf / aya / bpftool)              |
|     |  CO-RE再配置を適用、bpf()システムコール                  |
+-----|--------------------------------------------------------+
      v
カーネル空間(Kernel space)
+--------------------------------------------------------------+
|  Verifier ──> 安全性検証 (ポインタ、境界、終了保証)            |
|     |  通過                                                  |
|     v                                                        |
|  JITコンパイラ ──> ネイティブ機械語へ変換                      |
|     |                                                        |
|     v                                                        |
|  フック(hook)へアタッチ                                       |
|   ├── kprobe / kretprobe (カーネル関数の入口/出口)            |
|   ├── tracepoint (安定したカーネルイベント)                    |
|   ├── XDP (NICドライバ受信直後)                               |
|   ├── tc (トラフィック制御 ingress/egress)                    |
|   ├── LSM (セキュリティフック)                                |
|   └── cgroup (ソケット/システムコール制御)                     |
|                                                              |
|  マップ(Maps) <────> ユーザー空間とデータ共有                  |
+--------------------------------------------------------------+

流れを要約すると次のとおりです。制限付きCで書いたコードをclangがBPFバイトコードへコンパイルし、ローダーがbpf()システムコールでカーネルに渡すと、Verifierが安全性を検証し、JITがネイティブコードへ変換して指定のフックにアタッチします。プログラムとユーザー空間はマップを通じてデータをやり取りします。

プログラムタイプの地図

eBPFプログラムはアタッチされる場所(フック)によってタイプが分かれ、タイプごとに受け取れるコンテキストと使えるヘルパーが異なります。実務でよく出会うタイプを整理します。

プログラムタイプアタッチポイント主な用途特徴
kprobe / kretprobe任意のカーネル関数の入口/出口トレーシング、デバッグ柔軟だがカーネルバージョンで関数が変わり得る
tracepoint事前定義された静的カーネルイベント安定したトレーシングABIが比較的安定、推奨の出発点
fentry / fexitカーネル関数の入口/出口 (BTFベース)高性能トレーシングkprobeよりオーバーヘッドが低い、カーネル5.5以降
XDPNICドライバ受信経路の最前段DDoS防御、ロードバランシングsk_buff割り当て前のため非常に高速
tc (clsact)トラフィック制御 ingress/egressパケット操作、ポリシー送信方向も処理可能、sk_buffへアクセス
LSM (BPF LSM)Linuxセキュリティモジュールフックランタイムセキュリティポリシー動作のブロックが可能、カーネル5.7以降
cgroup系cgroup単位のソケット/システムコールコンテナごとのネットワークポリシーコンテナ環境と相性が良い
uprobe / USDTユーザー空間関数/静的トレースポイントアプリケーショントレーシングライブラリ関数の追跡など
perf_eventタイマー/PMUイベントCPUプロファイリングサンプリングベースの分析

選択基準を簡単にまとめます。安定性が重要ならtracepoint、カーネル内部の細かいポイントが必要ならkprobeやfentry、パケットを最速で処理すべきならXDP、ポリシーによるブロックが目的ならLSMをまず検討します。

マップ: カーネルとユーザー空間の橋

マップはeBPFプログラムが状態を保存し、ユーザー空間とデータを交換するキー・バリューのデータ構造です。どのマップを使うかがプログラムの性能と構造を左右します。

マップタイプ構造代表的な用途
BPF_MAP_TYPE_HASHハッシュテーブルPID別統計、コネクション追跡など任意キーの検索
BPF_MAP_TYPE_ARRAY固定サイズ配列設定値、インデックスベースのカウンタ
BPF_MAP_TYPE_PERCPU_HASHCPU別ハッシュロック競合なしの高頻度カウント
BPF_MAP_TYPE_PERCPU_ARRAYCPU別配列ヒストグラムのバケット、ホットパス統計
BPF_MAP_TYPE_LRU_HASHLRUハッシュ満杯時に古い項目を自動削除
BPF_MAP_TYPE_RINGBUFリングバッファ (MPSC)カーネル→ユーザーのイベントストリーミング (5.8以降、推奨)
BPF_MAP_TYPE_PERF_EVENT_ARRAYCPU別perfバッファ旧カーネルでのイベント伝達
BPF_MAP_TYPE_LPM_TRIE最長プレフィックス一致IP CIDRマッチング
BPF_MAP_TYPE_PROG_ARRAYプログラム配列tail callによるプログラムチェーン
BPF_MAP_TYPE_SK_STORAGEソケットローカルストレージソケットごとのメタデータ

実務上のヒントを二つ強調しておきます。

  1. イベント伝達にはringbufを優先します。perfバッファと違いCPU間のイベント順序が保証され、メモリ効率が良く、ユーザー空間APIもシンプルです。
  2. ホットパスのカウンタはpercpuマップで作ります。通常のハッシュマップを複数CPUが同時に更新するとアトミック演算のコストが大きくなりますが、percpuマップはCPUごとに独立したスロットを使うため競合がありません。集計はユーザー空間で読む時に行います。

マップの定義は、最新のlibbpfスタイルではBTFベースのセクションで宣言します。

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, __u32);     /* PID */
    __type(value, __u64);   /* 呼び出し回数 */
} call_count SEC(".maps");

Verifier: カーネルの門番

VerifierはeBPFの安全性を担う中核コンポーネントです。プログラムのロード時にすべての実行パスを静的に解析し、次を検証します。

  • プログラムが必ず終了するか (無限ループの禁止)
  • すべてのメモリアクセスが検証済みの範囲内か
  • 未初期化のレジスタを読んでいないか
  • そのプログラムタイプで許可されたヘルパーのみを呼んでいるか
  • ポインタ演算が安全な範囲を逸脱していないか

境界チェック: Verifierが見る世界

Verifierは各レジスタの取り得る値の範囲を追跡します。パケットデータを読むXDPプログラムの典型的なパターンは次のとおりです。

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

    /* この境界チェックがないとVerifierがロードを拒否する */
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    /* チェックを通過した後にのみ eth->h_proto へのアクセスが許される */
    if (eth->h_proto == bpf_htons(ETH_P_IP))
        return XDP_DROP;

    return XDP_PASS;
}

if文で境界をチェックした瞬間、Verifierはその分岐内でポインタの範囲が安全であることを「知る」ことになります。コンパイラがこのチェックを最適化で除去したり順序を入れ替えたりすると検証が失敗し得るため、境界チェックのコードはシンプルかつ直接的に書くのが良いです。

ループの制約

初期のeBPFはループを一切許可しませんでした。現在は段階的に緩和されています。

方法カーネルバージョン説明
pragma unroll全バージョンコンパイル時にループを展開、回数の固定が必要
bounded loop5.3以降Verifierが終了を証明できる有界ループを許可
bpf_loopヘルパー5.17以降コールバックベースのループ、大きな反復回数に有利
open-coded iterator6.4以降bpf_forマクロなどイテレータベースの反復

よくある検証失敗と解決法

エラーメッセージ (要旨)原因解決
invalid mem access境界チェックなしのポインタ参照アクセス前に明示的な境界チェックを追加
unbounded loop detected終了を証明できないループ反復回数に上限を付与、bpf_loopを使用
BPF program is too large命令数の上限超過ロジック分割、tail call、5.2以降は上限が百万に緩和
stack limit exceededスタック512バイト超過大きな構造体はpercpu arrayをスクラッチ領域に
R1 type=ctx expected=fpコンテキストポインタの誤用コンテキストフィールドは決められた方法でのみアクセス
helper call is not allowedプログラムタイプで禁止されたヘルパーそのタイプで許可されるヘルパーを確認

Verifierのログは長く難解なことで有名ですが、失敗地点直前のレジスタ状態ダンプを追えば、ほとんどの場合原因を見つけられます。libbpfローダーでverboseログを有効にする方法はデバッグの節で扱います。

初めてのプログラム実習: libbpf + CO-RE

実際に動くプログラムを作ってみましょう。目標は「システム全体で実行されるすべてのプロセス(execve)を追跡し、PID、親PID、コマンド名を出力する」ミニexecsnoopです。

CO-REとvmlinux.h、BTFとは

従来のbcc方式は、対象マシンにclangとカーネルヘッダをインストールしてランタイムにコンパイルしていました。CO-RE(Compile Once, Run Everywhere)はこの問題を解決します。

  • BTF(BPF Type Format): カーネルが自分自身の型情報をコンパクトに内蔵したフォーマットです。/sys/kernel/btf/vmlinux ファイルがあればBTFが有効なカーネルです。
  • vmlinux.h: カーネルBTFから生成された、すべてのカーネル型定義を含む単一ヘッダです。カーネルヘッダのパッケージなしでカーネル構造体を参照できるようにしてくれます。
  • CO-RE再配置: コンパイル時に構造体フィールドへのアクセスを「再配置情報」として記録しておき、ロード時にlibbpfが実行中カーネルのBTFと照合してフィールドオフセットを補正します。おかげで一度ビルドしたバイナリが、フィールド配置の異なる複数のカーネルでそのまま動きます。

vmlinux.hは次のコマンドで生成します。

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

カーネルサイドのコード (execsnoop.bpf.c)

// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>

#define TASK_COMM_LEN 16

struct event {
    __u32 pid;
    __u32 ppid;
    char comm[TASK_COMM_LEN];
};

/* カーネル -> ユーザー空間のイベント伝達用リングバッファ */
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_execve")
int handle_execve(struct trace_event_raw_sys_enter *ctx)
{
    struct event *e;
    struct task_struct *task;

    /* リングバッファにイベント領域を予約 */
    e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e)
        return 0;

    e->pid = bpf_get_current_pid_tgid() >> 32;

    /* CO-REマクロでtask_structから親PIDを読む */
    task = (struct task_struct *)bpf_get_current_task();
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);

    bpf_get_current_comm(&e->comm, sizeof(e->comm));

    bpf_ringbuf_submit(e, 0);
    return 0;
}

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

ポイントを押さえておきましょう。

  • SECマクロはELFセクション名でプログラムタイプとアタッチポイントを宣言します。
  • BPF_CORE_READはCO-RE再配置が適用される安全なメンバーアクセスマクロです。ネストしたポインタチェーン(task → real_parent → tgid)を一度にたどれます。
  • ライセンス宣言は必須です。GPL互換ライセンスでないと一部のヘルパーが使えません。

ユーザーサイドのローダー (execsnoop.c)

ビルド過程でbpftoolが.bpf.oからスケルトンヘッダ(execsnoop.skel.h)を生成してくれます。スケルトンはロード/アタッチ/解放を型安全な関数でラップします。

// SPDX-License-Identifier: GPL-2.0
#include <stdio.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include "execsnoop.skel.h"

struct event {
    __u32 pid;
    __u32 ppid;
    char comm[16];
};

static volatile sig_atomic_t exiting = 0;

static void sig_handler(int sig) { exiting = 1; }

static int handle_event(void *ctx, void *data, size_t len)
{
    const struct event *e = data;
    printf("%-8u %-8u %-16s\n", e->pid, e->ppid, e->comm);
    return 0;
}

int main(void)
{
    struct execsnoop_bpf *skel;
    struct ring_buffer *rb;
    int err;

    signal(SIGINT, sig_handler);
    signal(SIGTERM, sig_handler);

    skel = execsnoop_bpf__open_and_load();
    if (!skel) {
        fprintf(stderr, "BPFスケルトンのロードに失敗\n");
        return 1;
    }

    err = execsnoop_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "BPFプログラムのアタッチに失敗: %d\n", err);
        goto cleanup;
    }

    rb = ring_buffer__new(bpf_map__fd(skel->maps.events),
                          handle_event, NULL, NULL);
    if (!rb) {
        err = -1;
        goto cleanup;
    }

    printf("%-8s %-8s %-16s\n", "PID", "PPID", "COMM");
    while (!exiting) {
        err = ring_buffer__poll(rb, 100 /* ms */);
        if (err == -EINTR) { err = 0; break; }
        if (err < 0) break;
    }

cleanup:
    ring_buffer__free(rb);
    execsnoop_bpf__destroy(skel);
    return err < 0 ? 1 : 0;
}

ビルドと実行

# 1. vmlinux.h を生成
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

# 2. カーネルサイドをコンパイル (-g 必須: BTF/CO-RE情報を生成)
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
      -c execsnoop.bpf.c -o execsnoop.bpf.o

# 3. スケルトンヘッダを生成
bpftool gen skeleton execsnoop.bpf.o > execsnoop.skel.h

# 4. ユーザーサイドをコンパイルしてリンク
clang -O2 -o execsnoop execsnoop.c -lbpf -lelf -lz

# 5. 実行 (rootまたは適切なcapabilityが必要)
sudo ./execsnoop

別のターミナルでlsやdateを実行すると、PID、PPID、コマンド名が即座に出力されるのを確認できます。この小さなプログラムが、ほぼゼロに近いオーバーヘッドでシステム全体のexecveを観察しているという事実こそが、eBPFの力です。

bpftool: eBPFのスイスアーミーナイフ

bpftoolはカーネルツリーで管理される公式CLIで、ロード済みのプログラムとマップを調べるのに欠かせません。

# ロードされた全プログラムを一覧
sudo bpftool prog show

# 特定プログラムのJIT結果を逆アセンブル
sudo bpftool prog dump jited id 42

# 変換後(Verifier通過後)のバイトコードを確認
sudo bpftool prog dump xlated id 42

# マップの一覧と内容のダンプ
sudo bpftool map show
sudo bpftool map dump id 17

# システムのeBPF機能サポート状況を調査
sudo bpftool feature probe

# ネットワークにアタッチされたプログラムを確認 (XDP, tc)
sudo bpftool net show

# プログラムをbpffsにピン(pin)してローダー終了後も維持
sudo bpftool prog pin id 42 /sys/fs/bpf/myprog

特にfeature probeは「このカーネルでどのプログラムタイプ、マップタイプ、ヘルパーがサポートされるか」を一度に見せてくれるため、新しいサーバーで作業を始める際に最初に実行する価値があります。

言語とフレームワークの選択

eBPF自体はカーネル技術であり、どの言語で開発するかは別の選択です。

フレームワーク言語長所短所向いているケース
libbpf + CCカーネルツリー公式、機能が最新、CO-RE標準Cの生産性、メモリ管理の負担システムツール、最高性能と最新機能
cilium/ebpfGoGoエコシステム統合、純Goローダーカーネルサイドは依然Ck8sツール、Goベースのエージェント
ayaRustカーネル/ユーザー両方Rust、メモリ安全エコシステムが比較的若いRustチーム、安全性重視のプロジェクト
bccPython + C豊富な例、素早いプロトタイピングランタイムコンパイル、重い依存関係学習、単発の分析
bpftrace専用DSLワンライナーで即座に分析複雑なロジックには限界運用中の即席トラブルシューティング

推奨ルートは明確です。運用分析はbpftraceで始め、製品化するツールはlibbpf(C)やcilium/ebpf(Go)、aya(Rust)で書きます。bccのランタイムコンパイル方式は、CO-REが普及した今では新規プロジェクトには推奨されません。

カーネルバージョンと機能マトリクス

デプロイ対象のカーネルが何をサポートするかは、設計段階で最初に確認すべき事項です。主要なマイルストーンを整理します(私が確実に知っている範囲であり、正確なサポート状況はbpftool feature probeで確認するのが安全です)。

カーネルバージョン主な機能
3.18eBPFシステムコールの導入
4.1〜4.7kprobe、tc、tracepointアタッチのサポート拡大
4.8XDPの導入
4.18BTFの導入開始
5.2命令数上限が百万に緩和、グローバル変数のサポート
5.3有界ループ(bounded loop)の許可
5.5fentry / fexit (BTFベースのトランポリン)
5.7BPF LSM、struct_ops
5.8ring bufferマップ、CAP_BPF権限の分離
5.10sleepable BPFプログラム
5.17bpf_loopヘルパー
6.xopen-coded iterator、kfuncの拡大、arenaなど継続的な拡張

実務基準では、RHEL 9、Ubuntu 22.04 LTS以降であればringbuf、CO-RE、fentryまで問題なく使えます。2026年現在の主流ディストリビューションのカーネル(5.14以降)では、この記事の例はすべて動作します。

デバッグのヒント

  1. Verifierログを詳しく見ます。libbpfの環境変数またはコードでverboseレベルを上げられます。
/* ロード前にデバッグ出力を有効化 */
libbpf_set_print(libbpf_print_fn);  /* LIBBPF_DEBUGレベルまで出力 */
  1. bpf_printkでカーネルサイドのprintfデバッグを行います。出力はtrace_pipeから読みます。
bpf_printk("pid=%d comm=%s", pid, comm);
sudo cat /sys/kernel/debug/tracing/trace_pipe
  1. コンパイラ最適化が境界チェックを除去する問題は、volatileの使用やbarrier_varマクロで回避します。
  2. xlatedダンプとソースを照合します。bpftool prog dump xlatedの出力でVerifierが見た実際の命令を確認すると、「自分が書いたコード」と「Verifierが検証したコード」の違いを発見できます。
  3. 小さく始めて段階的に育てます。一度に大きなプログラムを通そうとせず、最小バージョンを先にロードしてからロジックを追加します。

プロダクションでの考慮事項

オーバーヘッド

eBPFは軽量ですが、タダではありません。コストの大部分はフック自体の呼び出し頻度に比例します。

  • 毎秒数百万回呼ばれる関数(例: スケジューラのホットパス)にkprobeを仕掛けると、累積オーバーヘッドが目に見えることがあります。fentryはkprobeより低コストなので、可能であればfentryを使います。
  • マップ更新コストを下げるには、percpuマップと「集計してから渡す」パターン(カーネル側で集計し、ユーザー空間は定期的に読むだけ)を使います。
  • イベントが殺到し得るポイントでは、カーネルサイドでサンプリングやフィルタリングを行い、ringbufへ送る量自体を減らします。

権限: CAP_BPFとその仲間たち

カーネル5.8から、eBPFの権限がCAP_SYS_ADMINから分離されました。

権限許可範囲
CAP_BPFbpf()システムコールの基本使用 (マップ作成、一部のプログラムロード)
CAP_PERFMONトレーシング系プログラムのアタッチ、カーネルメモリの読み取り
CAP_NET_ADMINXDP、tcなどネットワークプログラムのアタッチ
CAP_SYS_ADMIN上記すべてを含む従来方式 (避けるべき)

可観測性エージェントをデプロイする際は、CAP_BPF + CAP_PERFMONの組み合わせで最小権限を構成し、ネットワークプログラムが必要な場合にのみCAP_NET_ADMINを追加するのがベストプラクティスです。また、unprivileged BPFはセキュリティ上ほとんどのディストリビューションで無効化されており(sysctl kernel.unprivileged_bpf_disabled)、そのままにしておくことを推奨します。

ライフサイクル管理

  • プログラムとマップは、ファイルディスクリプタの参照がなくなると解放されます。ローダープロセスから独立して維持するにはbpffsにピンする必要があります。
  • カーネルアップグレード時、CO-REのおかげでほとんど再ビルドは不要ですが、kprobe対象の関数が消えたり名前が変わったりすることがあるため、アタッチ失敗へのフォールバックロジックをエージェントに入れておくのが安全です。

学習ロードマップ

  1. ステージ1 — ユーザーとして始める: bpftraceワンライナーとbccツール(execsnoop、opensnoop、biolatency)を運用マシンの分析に活用し、感覚をつかみます。
  2. ステージ2 — 読む: ebpf.ioのWhat is eBPFドキュメントとカーネルBPFドキュメントを読み、プログラムタイプとマップの全体像を把握します。
  3. ステージ3 — 初めてのプログラム: この記事のexecsnoopのように、tracepoint + ringbufの組み合わせでlibbpfプログラムを書きます。libbpf-bootstrapリポジトリのテンプレートが素晴らしい出発点です。
  4. ステージ4 — 領域拡張: 関心分野に応じてXDP(ネットワーキング)、uprobe/USDT(アプリケーション)、LSM(セキュリティ)へ拡張します。
  5. ステージ5 — プロダクション化: CO-RE互換性マトリクス、権限設計、オーバーヘッド測定、CIでのマルチカーネルテストまで整えます。

落とし穴とアンチパターン

  • アンチパターン1: 不安定なカーネル内部関数へのkprobe依存。カーネルのマイナーアップデートで関数がインライン化されたり名前が変わったりすると、ツールが静かに壊れます。tracepointやfentry + BTF存在チェックで防御します。
  • アンチパターン2: マップサイズ不足の放置。ハッシュマップが満杯になると更新が失敗しデータが失われます。LRUマップを使うか、失敗カウンタを別途設けます。
  • アンチパターン3: ringbufの消費遅延。ユーザー空間のコンシューマが遅いとreserveが失敗しイベントが消えます。ドロップカウンタを作って監視すべきです。
  • アンチパターン4: 検証通過だけを目的としたコードのねじ曲げ。Verifierを「だます」パターンはカーネル/コンパイラのバージョンが変わると壊れます。明示的な境界チェックとシンプルな制御フローが正解です。
  • アンチパターン5: ライセンス宣言の欠落。GPL宣言がないと多数のヘルパーがブロックされます。意図的に非GPLを選ぶ場合は、使用可能なヘルパー一覧を事前に確認します。

おわりに

eBPFは「カーネルを直さずにカーネルの動作を変える」という、十年前なら不可能と思われたことを日常にしました。核心は三つでした。フックにアタッチされるプログラム、データをつなぐマップ、そして安全を保証するVerifierです。この三つの関係を理解すれば、どんなeBPFツールに出会っても内部動作を推論できます。

次の記事では、この基礎の上でbpftraceとBCCツールを使って実際のシステムのブラックボックスを開ける可観測性の実践を扱い、その次の記事ではTetragonとFalco、BPF LSMでランタイムセキュリティを構築する方法を見ていきます。

参考資料