eBPF 完整原理:User Space ↔ Kernel Space 互動機制
一句話說完 eBPF
eBPF 就是讓你「安全地在 Linux kernel 裡面塞一小段自訂程式」, 不用改 kernel 原始碼、不用載入傳統 kernel module,就能觀察或改變 kernel 的行為。
全景圖:eBPF 從編寫到執行的完整生命週期
┌─────────────────────────────────────────────────────────────────────┐
│ USER SPACE (使用者空間) │
│ │
│ ① 你寫的 eBPF 程式 (C / Rust) │
│ my_prog.bpf.c │
│ │ │
│ ▼ │
│ ② Clang/LLVM 編譯器 │
│ 把 C 編譯成 eBPF bytecode(一種特殊的機器碼) │
│ │ │
│ ▼ │
│ ③ Loader 載入器 (libbpf / bpftrace / bcc) │
│ 呼叫 bpf() system call,把 bytecode 送進 kernel │
│ │ │
│ │ bpf() syscall │
│ ════════╪═══════════════════════════════════════════════════════════ │
│ │ KERNEL SPACE (核心空間) │
│ ▼ │
│ ④ Verifier 驗證器 │
│ 「這段程式安全嗎?會不會搞壞 kernel?」 │
│ - 不能有無限迴圈 │
│ - 不能存取非法記憶體 │
│ - 指令數量有上限 │
│ │ │
│ ▼ 驗證通過 ✓ │
│ ⑤ JIT Compiler (即時編譯器) │
│ 把 eBPF bytecode → 真正的 CPU 原生指令 (x86/ARM) │
│ (白話:翻譯成 CPU 直接看得懂的語言,跑超快) │
│ │ │
│ ▼ │
│ ⑥ 掛載到 Hook Point (掛鉤點) │
│ 程式被「釘」在 kernel 的某個事件點上 │
│ 每次該事件發生,你的程式就自動執行一次 │
│ │ │
│ ▼ │
│ ⑦ eBPF Maps (共享資料結構) │
│ kernel 裡的「共用黑板」,程式寫結果、user space 來讀 │
│ │
└─────────────────────────────────────────────────────────────────────┘
核心互動機制:User Space ↔ Kernel Space 的三條通道
USER SPACE KERNEL SPACE
┌──────────────────┐ ┌──────────────────┐
│ │ ┌─────────────────────┐ │ │
│ 你的應用程式 │ │ 通道 ① bpf() 系統呼叫 │ │ eBPF 子系統 │
│ (Python/Go/C) │───>│ 「把程式送進 kernel」 │───>│ │
│ │ └─────────────────────┘ │ Verifier │
│ │ │ JIT Compiler │
│ │ ┌─────────────────────┐ │ eBPF VM │
│ 讀取結果 │<───│ 通道 ② eBPF Maps │<───│ │
│ (統計/追蹤資料) │ │ 「共享資料黑板」 │ │ eBPF 程式執行 │
│ │ └─────────────────────┘ │ (寫入結果) │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ 即時事件串流 │<───│ 通道 ③ Ring Buffer │<───│ 事件發生時 │
│ (perf/ring buf) │ │ 「即時訊息管道」 │ │ 即時推送資料 │
│ │ └─────────────────────┘ │ │
└──────────────────┘ └──────────────────┘
白話解釋三條通道
| 通道 | 比喻 | 用途 |
|---|---|---|
| bpf() syscall | 你把一張「工作指令」交給 kernel 警衛 | 載入程式、建立 map、掛載到 hook |
| eBPF Maps | kernel 裡的一塊共用白板 | 雙向讀寫:kernel 寫統計、user 讀統計 |
| Ring Buffer | kernel 裝了一台監視器,即時串流給你看 | 單向推送:kernel → user,高速事件流 |
Hook Points:eBPF 程式能「釘」在哪裡?
┌─────────────────────────────────────────────────────────────────┐
│ LINUX KERNEL 內部 │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ 網路封包 │ │ 系統呼叫 │ │ 檔案系統 │ │
│ │ 收發路徑 │ │ 進出入口 │ │ 讀寫操作 │ │
│ │ │ │ │ │ │ │
│ │ XDP ────────│ │ tracepoint ──│ │ kprobe ───────│ │
│ │ TC ────────│ │ sys_enter ───│ │ fentry ───────│ │
│ │ socket ─────│ │ sys_exit ────│ │ fexit ────────│ │
│ └──────┬──────┘ └──────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 你的 eBPF 程式掛在這些點上 │ │
│ │ 每次事件觸發 → 自動執行你的程式一次 │ │
│ │ 就像在高速公路上裝了「測速照相機」 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 排程器 │ │ 記憶體管理 │ │ 硬體中斷 │ │
│ │ (Scheduler) │ │ (MM) │ │ (IRQ) │ │
│ │ │ │ │ │ │ │
│ │ sched_* ─────│ │ kprobe ──────│ │ tracepoint ──│ │
│ │ tracepoint ──│ │ fentry ──────│ │ perf_event ──│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
常見 Hook 類型白話對照
| Hook 類型 | 白話 | 典型用途 |
|---|---|---|
| kprobe | 在任意 kernel 函式「門口」裝竊聽器 | 追蹤任何 kernel function 被呼叫 |
| kretprobe | 在函式「出口」裝竊聽器 | 拿到 function 的回傳值 |
| uprobe | 跟 kprobe 一樣,但是裝在 user space 程式上 | 追蹤應用程式的函式呼叫 |
| tracepoint | kernel 開發者預先埋好的「官方觀測點」 | 穩定 API,不怕 kernel 升級壞掉 |
| fentry/fexit | kprobe 的進化版,更快更安全 | 新版 kernel 推薦用這個 |
| XDP | 網路封包一進網卡就攔截(最早的攔截點) | 超高速封包過濾/轉發 (DDoS 防禦) |
| TC | 網路封包在 Traffic Control 層攔截 | 流量整形、負載均衡 |
| perf_event | CPU 硬體效能計數器觸發 | CPU profiling、cache miss 分析 |
| LSM | Linux Security Module 的安全檢查點 | 自訂安全策略 |
Verifier 驗證器:為什麼 eBPF 很安全?
你的 eBPF bytecode
│
▼
┌──────────────────────────────────────────────────┐
│ VERIFIER (驗證器) │
│ │
│ 「我要逐行檢查你的程式,確保不會搞壞 kernel」 │
│ │
│ 檢查項目: │
│ ┌────────────────────────────────────────────┐ │
│ │ ✓ 程式會不會跑到停不下來?(無限迴圈檢測) │ │
│ │ → 所有迴圈必須有明確上限 │ │
│ │ → 白話:不能寫 while(true) │ │
│ ├────────────────────────────────────────────┤ │
│ │ ✓ 會不會亂讀亂寫記憶體?(記憶體安全檢查) │ │
│ │ → 每個指標都要檢查 != NULL │ │
│ │ → 白話:不能碰你不該碰的東西 │ │
│ ├────────────────────────────────────────────┤ │
│ │ ✓ 程式有沒有太大?(指令數上限) │ │
│ │ → 最多 100 萬條指令 (Linux 5.2+) │ │
│ │ → 白話:程式不能太肥 │ │
│ ├────────────────────────────────────────────┤ │
│ │ ✓ 有沒有用到不該用的 kernel 功能? │ │
│ │ → 只能呼叫白名單裡的 helper function │ │
│ │ → 白話:只能用 kernel 准你用的工具 │ │
│ └────────────────────────────────────────────┘ │
│ │
│ │ │ │
│ 驗證通過 ✓ 驗證失敗 ✗ │
│ │ │ │
│ ▼ ▼ │
│ 送去 JIT 編譯 回傳錯誤訊息給 │
│ 準備執行 user space │
│ 「你的程式不合格」 │
└──────────────────────────────────────────────────┘
白話比喻
Verifier 就像機場安檢:
- 你(eBPF 程式)要進入管制區(kernel)
- 安檢人員會檢查你身上有沒有危險物品
- 通過才能進去,沒通過就退回去
- 這就是為什麼 eBPF 比傳統 kernel module 安全得多
eBPF Maps:User ↔ Kernel 的共享資料結構
USER SPACE KERNEL SPACE
┌──────────────┐ ┌──────────────────────────┐
│ │ bpf() syscall │ │
│ map_fd = bpf│──────────────────────>│ Kernel 建立一個 Map │
│ (CREATE_MAP)│ 「幫我建一塊白板」 │ │
│ │ │ ┌────────────────────┐ │
│ │ │ │ eBPF Map │ │
│ │ bpf(MAP_LOOKUP) │ │ │ │
│ 讀取資料 │<─────────────────────>│ │ Key │ Value │ │
│ │ 「白板上寫了什麼?」 │ │ ───────┼──────── │ │
│ │ │ │ "eth0" │ 15342 │ │
│ 更新資料 │ bpf(MAP_UPDATE) │ │ "lo" │ 87 │ │
│ │──────────────────────>│ │ "wlan0"│ 8821 │ │
│ │ 「我要改白板內容」 │ │ │ │
│ │ │ └─────────┬──────────┘ │
│ │ │ │ │
│ │ │ │ eBPF 程式 │
│ │ │ │ 也能讀寫 │
│ │ │ ▼ │
│ │ │ ┌────────────────────┐ │
│ │ │ │ eBPF 程式執行中 │ │
│ │ │ │ 每收到一個封包: │ │
│ │ │ │ count[iface]++ │ │
│ │ │ │ 寫回 Map │ │
│ │ │ └────────────────────┘ │
└──────────────┘ └──────────────────────────┘
常見 Map 類型
┌─────────────────────────────────────────────────────────────┐
│ eBPF Map 類型一覽 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ HASH Map │ │ ARRAY Map │ │ RING BUFFER │ │
│ │ │ │ │ │ │ │
│ │ key → value │ │ idx → value │ │ kernel ──push──> │ │
│ │ 像 dict │ │ 像 array │ │ user ──pop──> │ │
│ │ │ │ │ │ 高速事件串流 │ │
│ │ 用途: │ │ 用途: │ │ │ │
│ │ 統計/查表 │ │ 設定/計數 │ │ 用途: │ │
│ └─────────────┘ └─────────────┘ │ 即時事件通知 │ │
│ └─────────────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ LRU Hash │ │ Per-CPU │ │ STACK TRACE │ │
│ │ │ │ Hash/Array │ │ │ │
│ │ 自動淘汰 │ │ 每個 CPU │ │ 記錄呼叫堆疊 │ │
│ │ 最少使用的 │ │ 一份副本 │ │ │ │
│ │ 白話:滿了 │ │ 白話:避免 │ │ 用途: │ │
│ │ 自動丟舊的 │ │ CPU 搶鎖 │ │ profiling │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
完整執行流程:一個封包追蹤程式的一生
時間軸 ──────────────────────────────────────────────────────────>
USER SPACE:
Step 1: 寫程式 Step 5: 讀結果
┌──────────────┐ ┌──────────────┐
│ // 計算每個 │ │ 每秒讀 Map │
│ // IP 的封包數 │ │ 印出統計報表 │
│ SEC("xdp") │ │ │
│ int count( │ │ 10.0.0.1: 53 │
│ ctx) { │ │ 10.0.0.2: 17 │
│ ... │ │ 10.0.0.3: 89 │
│ } │ │ │
└──────┬───────┘ └──────▲───────┘
│ │
Step 2: 編譯 Step 5: bpf(MAP_LOOKUP)
│ │
┌──────▼───────┐ │
│ clang -O2 │ │
│ -target bpf │ │
│ → .o 檔案 │ │
└──────┬───────┘ │
│ │
Step 3: 載入 │
│ bpf() syscall │
═══════╪════════════════════════════════╪═══════════════
│ KERNEL SPACE │
▼ │
┌──────────────┐ │
│ Verifier │ │
│ 檢查安全性 │ │
└──────┬───────┘ │
│ 通過 ✓ │
▼ │
┌──────────────┐ │
│ JIT 編譯 │ │
│ → 原生指令 │ │
└──────┬───────┘ ┌──────┴───────┐
│ │ eBPF Map │
▼ │ (Hash Map) │
┌──────────────┐ │ │
│ 掛載到 XDP │ Step 4: 執行 │ key │ val │
│ hook point │────────────────>│ 10..1 │ 53 │
│ │ 每個封包進來時 │ 10..2 │ 17 │
│ 網卡收到封包 │ 自動計數+1 │ 10..3 │ 89 │
│ → 觸發執行 │ 寫入 Map │ │
└──────────────┘ └──────────────┘
Helper Functions:eBPF 程式能用的 Kernel 工具箱
┌──────────────────────────────────────────────────────────────┐
│ eBPF Helper Functions (白名單工具箱) │
│ │
│ 你的 eBPF 程式不能直接呼叫 kernel function │
│ 只能透過這些「特許工具」間接操作 │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 📦 Map 操作 │ │ 📡 網路操作 │ │
│ │ │ │ │ │
│ │ bpf_map_lookup │ │ bpf_redirect │ │
│ │ bpf_map_update │ │ bpf_skb_store │ │
│ │ bpf_map_delete │ │ bpf_csum_diff │ │
│ │ │ │ bpf_clone_redirect│ │
│ │ 白話:讀寫白板 │ │ │ │
│ └──────────────────┘ │ 白話:改封包/轉發 │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ ⏱ 時間/隨機 │ │ 🔍 追蹤/除錯 │ │
│ │ │ │ │ │
│ │ bpf_ktime_get_ns │ │ bpf_trace_printk │ │
│ │ bpf_get_prandom │ │ bpf_get_stackid │ │
│ │ │ │ bpf_probe_read │ │
│ │ 白話:看時鐘/ │ │ │ │
│ │ 擲骰子 │ │ 白話:印 log / │ │
│ └──────────────────┘ │ 讀資料 │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 👤 行程資訊 │ │ 🔐 安全相關 │ │
│ │ │ │ │ │
│ │ bpf_get_current │ │ bpf_get_current │ │
│ │ _pid_tgid │ │ _cgroup_id │ │
│ │ bpf_get_current │ │ bpf_sk_lookup │ │
│ │ _comm │ │ │ │
│ │ │ │ 白話:查權限/ │ │
│ │ 白話:查是誰在 │ │ 查 cgroup │ │
│ │ 跑這個程式 │ └──────────────────┘ │
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────────┘
eBPF 程式類型 vs 掛載點 對照
┌─────────────────┬──────────────────┬─────────────────────────┐
│ 程式類型 │ 掛載位置 │ 白話用途 │
├─────────────────┼──────────────────┼─────────────────────────┤
│ │ │ │
│ XDP │ 網卡驅動層 │ 封包一進來就處理 │
│ │ (最底層) │ 速度最快,DDoS 防禦 │
│ │ │ │
│ TC (clsact) │ Traffic Control │ 流量分類/整形 │
│ │ (L2/L3) │ 負載均衡、流量鏡像 │
│ │ │ │
│ Socket Filter │ Socket 層 │ 過濾 socket 收到的封包 │
│ │ │ tcpdump 就是用這個 │
│ │ │ │
│ kprobe │ 任意 kernel func│ 追蹤 kernel 函式呼叫 │
│ │ │ 最靈活但可能不穩定 │
│ │ │ │
│ tracepoint │ kernel 預定義點 │ 穩定的觀測點 │
│ │ │ kernel 升級不會壞 │
│ │ │ │
│ uprobe │ user space func │ 追蹤應用程式函式 │
│ │ │ 不改程式碼就能觀測 │
│ │ │ │
│ perf_event │ CPU PMU 計數器 │ CPU profiling │
│ │ │ 效能瓶頸分析 │
│ │ │ │
│ LSM │ 安全檢查點 │ 自訂安全策略 │
│ │ │ 比 SELinux 更靈活 │
│ │ │ │
│ cgroup │ cgroup 事件 │ 容器級別的網路/資源控制 │
│ │ │ Kubernetes 必備 │
│ │ │ │
└─────────────────┴──────────────────┴─────────────────────────┘
網路封包在 Kernel 中的 eBPF 攔截點
網路封包的旅程
(從網卡到應用程式)
外部網路
│
▼
┌─────────┐
│ 網卡 │ NIC (Network Interface Card)
│ (NIC) │
└────┬────┘
│
▼
╔═══════════╗
║ XDP ║ ← eBPF 攔截點 ① (最早!封包剛進網卡驅動)
║ ║ 可以:丟棄 XDP_DROP / 轉發 XDP_TX / 放行 XDP_PASS
║ ║ 白話:「門口警衛,壞人直接擋掉,不用進屋」
╚═════╤═════╝
│ XDP_PASS (放行)
▼
┌───────────┐
│ sk_buff │ kernel 把封包包裝成 sk_buff 結構
│ 建立 │ (白話:把包裹貼上標籤,方便內部處理)
└─────┬─────┘
│
▼
╔═══════════╗
║ TC ║ ← eBPF 攔截點 ② (Traffic Control 層)
║ ingress ║ 可以:修改封包 / 重導 / 丟棄 / 標記
║ ║ 白話:「分貨中心,決定包裹走哪條路」
╚═════╤═════╝
│
▼
┌───────────┐
│ Netfilter│ iptables / nftables 規則在這裡
│ (L3/L4) │
└─────┬─────┘
│
▼
╔═══════════╗
║ Socket ║ ← eBPF 攔截點 ③ (Socket 層)
║ Filter ║ 可以:過濾送到 socket 的封包
║ ║ 白話:「收件人的信箱過濾器」
╚═════╤═════╝
│
▼
┌───────────┐
│ 應用程式 │ recv() / read() 拿到資料
│ (User │
│ Space) │
└───────────┘
反方向(送出封包):
應用程式 send()
│
▼
╔═══════════╗
║ Socket ║ ← eBPF 可以攔截送出的封包
╚═════╤═════╝
│
▼
╔═══════════╗
║ TC ║ ← eBPF 攔截點 (egress)
║ egress ║ 白話:「出貨檢查站」
╚═════╤═════╝
│
▼
┌─────────┐
│ 網卡 │ → 送出封包到網路
└─────────┘
bpf() System Call:所有操作的唯一入口
USER SPACE 程式呼叫 bpf() 時要指定「我要做什麼」:
bpf(指令, 參數, 參數大小)
│
├── BPF_PROG_LOAD → 「載入一個新的 eBPF 程式」
│ 回傳 file descriptor (程式的代號)
│
├── BPF_MAP_CREATE → 「建立一個新的 Map」
│ 回傳 file descriptor (Map 的代號)
│
├── BPF_MAP_LOOKUP_ELEM → 「用 key 查 Map 裡的值」
│
├── BPF_MAP_UPDATE_ELEM → 「寫入/更新 Map 裡的值」
│
├── BPF_MAP_DELETE_ELEM → 「刪除 Map 裡的一筆資料」
│
├── BPF_PROG_ATTACH → 「把程式掛到某個 hook point」
│
├── BPF_PROG_DETACH → 「把程式從 hook point 拆下來」
│
├── BPF_LINK_CREATE → 「建立一個 link(更強的 attach)」
│
└── BPF_OBJ_PIN → 「把 Map/程式釘到 bpffs 檔案系統」
白話:存到 /sys/fs/bpf/ 讓別的程式也能用
┌──────────────────────────────────────────────────┐
│ 白話總結: │
│ │
│ bpf() syscall 就是 user space 和 kernel 之間 │
│ 的「唯一窗口」。你要做任何跟 eBPF 有關的事, │
│ 都必須透過這個窗口跟 kernel 說。 │
│ │
│ 就像銀行只有一個服務窗口: │
│ - 開戶 (CREATE_MAP) │
│ - 存錢 (MAP_UPDATE) │
│ - 查餘額 (MAP_LOOKUP) │
│ - 銷戶 (MAP_DELETE) │
│ - 設定自動轉帳 (PROG_ATTACH) │
└──────────────────────────────────────────────────┘
eBPF 虛擬機 (VM) 架構
┌─────────────────────────────────────────────────────────┐
│ eBPF Virtual Machine │
│ │
│ 暫存器 (Registers):11 個 64-bit 暫存器 │
│ ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬───┐│
│ │ R0 │ R1 │ R2 │ R3 │ R4 │ R5 │ R6 │ R7 │ R8 │ R9 │R10││
│ └─┬──┴─┬──┴─┬──┴────┴────┴────┴─┬──┴────┴────┴─┬──┴───┘│
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ 回傳值 │ 引數 1-5 │ callee- │ 堆疊 │
│ │ (傳給 helper │ saved │ 指標 │
│ │ 的參數) │ (不會被 │ (512 │
│ │ │ 覆蓋) │ bytes)│
│ ctx │ │ │ │
│ 指標 │ │ │ │
│ (掛鉤點 │ │ │
│ 的上下文) │ │ │
│ │
│ 指令集:64-bit 固定寬度指令 │
│ ┌──────────────────────────────────────────────┐ │
│ │ 算術:add, sub, mul, div, mod, neg │ │
│ │ 位元:and, or, xor, lsh, rsh │ │
│ │ 記憶體:load, store (1/2/4/8 bytes) │ │
│ │ 跳轉:je, jne, jgt, jge, jlt, jle, call, exit│ │
│ │ 原子:atomic add, atomic cmpxchg │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 堆疊空間:512 bytes(很小!要省著用) │
│ │
│ 白話:eBPF VM 就是一台「迷你電腦」住在 kernel 裡面, │
│ 有自己的暫存器和指令集,但功能刻意被限制,確保安全。 │
└─────────────────────────────────────────────────────────┘
JIT 編譯:從 bytecode 到原生指令
eBPF bytecode JIT Compiler 原生 CPU 指令
(中間碼) (即時編譯器) (直接執行)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ r1 = *(u32*) │ │ │ │ mov eax, │
│ (r6 + 16) │ │ bytecode │ │ [rdi+0x10] │
│ │ │ ↓ │ │ │
│ if r1 == 0 │─────────────>│ 翻譯成 │─────────────>│ test eax,eax │
│ goto end │ │ x86/ARM │ │ je 0x... │
│ │ │ 原生指令 │ │ │
│ r0 = 1 │ │ │ │ mov eax, 1 │
│ exit │ │ │ │ ret │
└──────────────┘ └──────────────┘ └──────────────┘
效能比較:
┌──────────────────────────────────────────────┐
│ 解釋執行 (interpreter) ████████████ 慢 │
│ JIT 編譯後 ███ 快! │
│ 原生 C 程式碼 ██ 最快 │
└──────────────────────────────────────────────┘
白話:JIT 就像「同步口譯」。
不用每次都翻譯,一次翻好,之後直接講當地語言。
所以 eBPF 程式幾乎跟直接寫在 kernel 裡一樣快。
eBPF 生態系工具對照
抽象程度
高 ▲
│ ┌─────────────────────────────────────────────────────┐
│ │ bpftrace │
│ │ 「一行指令就能追蹤」 │
│ │ 適合:快速除錯、臨時觀測 │
│ │ 例:bpftrace -e 'kprobe:do_sys_open { printf(...)}'│
│ └─────────────────────────────────────────────────────┘
│ ┌─────────────────────────────────────────────────────┐
│ │ BCC (BPF Compiler Collection) │
│ │ 「Python + C 混合寫」 │
│ │ 適合:寫觀測工具、快速原型 │
│ │ 例:tools/opensnoop.py 追蹤所有檔案開啟 │
│ └─────────────────────────────────────────────────────┘
│ ┌─────────────────────────────────────────────────────┐
│ │ libbpf + CO-RE │
│ │ 「寫一次編譯,到處執行」 │
│ │ 適合:正式產品、需要跨 kernel 版本的工具 │
│ │ 白話:eBPF 的「正規軍」寫法 │
│ └─────────────────────────────────────────────────────┘
│ ┌─────────────────────────────────────────────────────┐
│ │ 直接寫 eBPF bytecode + bpf() syscall │
│ │ 「手動組合語言」 │
│ │ 適合:學習原理、極致效能優化 │
│ │ 白話:除非你是 kernel hacker,否則不要這樣做 │
低 │ └─────────────────────────────────────────────────────┘
└──────────────────────────────────────────────────────────>
控制程度
CO-RE (Compile Once, Run Everywhere) 原理
先搞懂:eBPF 的「編譯」到底產生了什麼?
┌─────────────────────────────────────────────────────────────────┐
│ 常見誤解:「eBPF 編譯成 bytecode,就像 Java 一樣到處跑」 │
│ │
│ 事實是:bytecode 本身「不能」到處跑! │
│ CO-RE 的魔法不在 bytecode,而是在「載入時修補」 │
└─────────────────────────────────────────────────────────────────┘
讓我們一步一步看清楚:
Step 1:你寫的 C 程式碼
// trace_pid.bpf.c
SEC("kprobe/do_fork")
int trace_fork(struct pt_regs *ctx) {
struct task_struct *task = (void *)bpf_get_current_task();
pid_t pid = BPF_CORE_READ(task, pid); // ← 關鍵!用 CO-RE 巨集讀欄位
// ^^^^^^^^^^^^^^^^^^^^^^^^
// 不是寫 task->pid(這會寫死 offset)
// 而是說「我要讀 task_struct 裡叫 pid 的欄位」
bpf_printk("fork pid=%d", pid);
return 0;
}
Step 2:Clang 編譯產出什麼?
clang -O2 -target bpf -g -c trace_pid.bpf.c -o trace_pid.bpf.o
^^^ ^^
│ │
│ └── -g 產生除錯資訊(BTF 需要)
│
└── 目標是 bpf,不是 x86,不是 ARM
產出的是 eBPF bytecode
┌─────────────────────────────────────────────────────────────────┐
│ │
│ trace_pid.bpf.o(ELF 格式的目的檔) │
│ │
│ 裡面裝了三樣東西: │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ① eBPF Bytecode(指令區段) │ │
│ │ │ │
│ │ 這是一連串 64-bit 指令,長這樣: │ │
│ │ │ │
│ │ 指令 0: r1 = bpf_get_current_task() │ │
│ │ 指令 1: r2 = *(u32*)(r1 + 100) ← offset 100(暫時的)│ │
│ │ 指令 2: call bpf_printk │ │
│ │ 指令 3: r0 = 0 │ │
│ │ 指令 4: exit │ │
│ │ │ │
│ │ 注意:指令 1 裡的 offset 100 是用「編譯機器」的 kernel │ │
│ │ 算出來的。如果目標機器的 kernel 不同,100 就是錯的! │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ② BTF 資訊(型別描述區段) │ │
│ │ │ │
│ │ 記錄了程式用到的所有型別: │ │
│ │ - struct task_struct { pid_t pid; char comm[16]; ... } │ │
│ │ - 每個欄位的名稱、型別、大小 │ │
│ │ │ │
│ │ 白話:「我這個程式認識的 kernel 結構長什麼樣」 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ③ CO-RE Relocation 記錄(重定位標記) │ │
│ │ │ │
│ │ 這是 CO-RE 的靈魂!記錄了: │ │
│ │ │ │
│ │ 「指令 1 的 offset 100,對應的是 │ │
│ │ struct task_struct 裡面叫做 pid 的欄位」 │ │
│ │ │ │
│ │ 白話:不是記「門牌號碼 100」, │ │
│ │ 而是記「住在這條街上叫王先生的那戶」 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Step 3:載入時 libbpf 的「修補魔法」(這才是 CO-RE 的核心!)
.bpf.o 檔案被搬到另一台機器(不同 kernel 版本)
┌─ 你的程式(User Space)────────────────────────────────────────┐
│ │
│ libbpf 載入 trace_pid.bpf.o │
│ │ │
│ │ Step A: 讀出 .bpf.o 裡的 eBPF bytecode │
│ │ Step B: 讀出 .bpf.o 裡的 BTF(程式認識的結構) │
│ │ Step C: 讀出 .bpf.o 裡的 CO-RE relocation 記錄 │
│ │ Step D: 讀取目標機器的 kernel BTF ←──────────────────┐ │
│ │ (在 /sys/kernel/btf/vmlinux) │ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────────────────────────────────────────┐ │ │
│ │ 比對! │ │ │
│ │ │ │ │
│ │ 程式的 BTF 說: │ │ │
│ │ task_struct.pid 在 offset 100 │ │ │
│ │ │ │ │
│ │ 目標機器的 kernel BTF 說: ◄──────────────┘ │ │
│ │ task_struct.pid 在 offset 108(多了一個新欄位) │ │
│ │ │ │ │
│ │ 差異:100 → 108,差了 8 bytes │ │ │
│ └──────────────────────────┬──────────────────────────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────────────────────────────────────────┐ │ │
│ │ 修補 bytecode! │ │ │
│ │ │ │ │
│ │ 原本:指令 1: r2 = *(u32*)(r1 + 100) ← 舊 offset │ │ │
│ │ │ │ │
│ │ 改成:指令 1: r2 = *(u32*)(r1 + 108) ← 新 offset │ │ │
│ │ ^^^ │ │ │
│ │ 直接改 bytecode 裡的數字! │ │ │
│ └──────────────────────────┬──────────────────────────┘ │ │
│ │ │ │
│ ▼ │ │
│ bpf(BPF_PROG_LOAD, 修補後的 bytecode) │ │
│ │ │ │
│ ════════════════════════════╪═══════════════════════════════ │ │
│ │ KERNEL │ │
│ ▼ │ │
│ Verifier → JIT → 執行 │ │
│ (用的是正確的 offset 108) │ │
└────────────────────────────────────────────────────────────────┘
完整流程圖:三個角色的分工
時間軸 ──────────────────────────────────────────────────────────────>
┌─────────────────┐ ┌───────────────────┐ ┌────────────────────┐
│ ① 開發者的電腦 │ │ ② 目標機器 │ │ ③ Kernel 內部 │
│ (編譯時) │ │ (載入時) │ │ (執行時) │
└────────┬────────┘ └─────────┬─────────┘ └─────────┬──────────┘
│ │ │
寫 C 程式碼 │ │
│ │ │
▼ │ │
clang 編譯 │ │
產出 .bpf.o: │ │
- bytecode │ │
- BTF (型別資訊) │ │
- relocation (重定位記錄) │ │
│ │ │
│ 把 .bpf.o │ │
│ 複製過去 │ │
├────────────────────>│ │
│ │ │
│ libbpf 載入 .bpf.o │
│ │ │
│ 讀取本機 kernel BTF │
│ (/sys/kernel/btf/vmlinux) │
│ │ │
│ 比對 BTF 差異 │
│ 修補 bytecode offset │
│ │ │
│ │ bpf() syscall │
│ ├─────────────────────>│
│ │ 送出修補後的 │
│ │ bytecode │
│ │ │
│ │ Verifier 驗證
│ │ JIT 編譯
│ │ 掛載到 hook
│ │ │
│ │ 每次事件觸發
│ │ 用正確 offset
│ │ 讀取 kernel 資料
│ │ │
│ │ Maps/Ring Buffer │
│ │<─────────────────────│
│ │ 回傳結果 │
│ │ │
白話總結:
┌─────────────────────────────────────────────────────────────────┐
│ CO-RE 的「到處跑」不是 bytecode 天生就能到處跑 │
│ 而是 libbpf 在載入的那一刻「臨場修補」bytecode │
│ │
│ 更準確地說: │
│ - Compile Once → 編譯一次,產出 .bpf.o(含 bytecode + 修補線索)│
│ - Run Everywhere → libbpf 根據目標 kernel 的 BTF 即時修補 │
│ 修補完的 bytecode 才送進 kernel │
└─────────────────────────────────────────────────────────────────┘
跟 Java 的比較:為什麼 eBPF 的「到處跑」不一樣?
┌─────────────────────────────┬──────────────────────────────────┐
│ Java │ eBPF CO-RE │
├─────────────────────────────┼──────────────────────────────────┤
│ │ │
│ .java → .class (bytecode) │ .bpf.c → .bpf.o (bytecode) │
│ │ │
│ JVM 解釋/JIT 執行 │ kernel 的 eBPF VM + JIT 執行 │
│ bytecode │ bytecode │
│ │ │
│ bytecode 本身就能跑 │ bytecode 本身「不能」跑 ✗ │
│ 因為 Java API 是固定的 │ 因為 kernel struct 會變 │
│ │ │
│ 不需要修補 │ libbpf 載入時必須修補 │
│ │ bytecode 裡的 offset │
│ │ │
│ 到處跑 = JVM 負責抽象 │ 到處跑 = libbpf 負責修補 │
│ │ + BTF 提供結構資訊 │
│ │ │
│ 比喻: │ 比喻: │
│ 「萬國語言翻譯機」 │ 「GPS 導航,到了當地 │
│ 到哪都能翻譯 │ 重新查地圖再出發」 │
│ │ │
└─────────────────────────────┴──────────────────────────────────┘
為什麼 eBPF 不能像 Java 那樣直接跑?
因為 eBPF 程式直接操作 kernel 的記憶體結構!
┌──────────────────────────────────────────────────────┐
│ │
│ Java 程式: │
│ String name = person.getName(); │
│ → JVM 幫你處理物件在記憶體裡的位置 │
│ → 你不用知道 name 在 offset 幾 │
│ │
│ eBPF 程式: │
│ pid = *(u32*)(task_struct_ptr + offset); │
│ → 你直接讀記憶體!offset 錯了就讀到垃圾! │
│ → 不同 kernel 版本 offset 不同 │
│ → 所以需要 CO-RE 在載入時修正 offset │
│ │
└──────────────────────────────────────────────────────┘
沒有 CO-RE 之前怎麼辦?(BCC 的做法)
┌─────────────────────────────────────────────────────────────┐
│ BCC 的做法:每台機器上「現場編譯」 │
│ │
│ ┌──────────────┐ │
│ │ Python 程式 │ 裡面嵌入 C 原始碼(字串) │
│ │ + 嵌入的 C │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ 每次執行時... │
│ ┌──────────────┐ │
│ │ 現場呼叫 │ 需要目標機器裝 clang + kernel headers │
│ │ clang 編譯 │ 編譯時直接用本機的 header → offset 一定對 │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 產出 bytecode │ 這個 bytecode 是針對本機 kernel 編譯的 │
│ │ → 載入執行 │ 所以 offset 一定正確 │
│ └──────────────┘ │
│ │
│ 缺點: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ❌ 每台機器都要裝 clang(幾百 MB) │ │
│ │ ❌ 每台機器都要裝 kernel-headers(要跟 kernel 版本配) │ │
│ │ ❌ 每次啟動都要重新編譯(幾秒到幾十秒) │ │
│ │ ❌ 編譯可能失敗(header 版本不對、clang 版本不對) │ │
│ │ ❌ 生產環境裝編譯器 = 增加攻擊面 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ vs CO-RE: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ✅ 只需要一個 .bpf.o 檔案(幾十 KB) │ │
│ │ ✅ 不需要裝 clang │ │
│ │ ✅ 不需要 kernel-headers │ │
│ │ ✅ 載入時修補只需幾毫秒 │ │
│ │ ✅ kernel 內建 BTF 就夠(CONFIG_DEBUG_INFO_BTF=y) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
BTF 深入理解
BTF = BPF Type Format(BPF 型別格式)
┌─────────────────────────────────────────────────────────────┐
│ │
│ BTF 存在兩個地方: │
│ │
│ ┌───────────────────────────────────┐ │
│ │ ① Kernel BTF(目標機器提供) │ │
│ │ │ │
│ │ 位置:/sys/kernel/btf/vmlinux │ │
│ │ │ │
│ │ 內容:這個 kernel 所有的 │ │
│ │ struct / union / enum / typedef │ │
│ │ 的完整描述 │ │
│ │ │ │
│ │ 白話:kernel 的「身體檢查報告」 │ │
│ │ 每個器官在哪、多大、什麼形狀 │ │
│ │ │ │
│ │ 啟用方式: │ │
│ │ CONFIG_DEBUG_INFO_BTF=y │ │
│ │ (大多數現代發行版預設開啟) │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ ② 程式 BTF(.bpf.o 裡面) │ │
│ │ │ │
│ │ 內容:這個 eBPF 程式「以為」 │ │
│ │ kernel struct 長什麼樣 │ │
│ │ (根據編譯時的 vmlinux.h 產生) │ │
│ │ │ │
│ │ 白話:程式帶的「舊地圖」 │ │
│ └───────────────────────────────────┘ │
│ │
│ CO-RE 做的事: │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ 程式的 BTF │ 比對 │ Kernel 的 BTF │ │
│ │ (舊地圖) │ ◄─────► │ (新地圖) │ │
│ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │
│ │ task_struct.pid │ task_struct.pid │
│ │ offset = 100 │ offset = 108 │
│ │ │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ▼ │
│ 差異:+8 bytes │
│ → 修補 bytecode 裡所有 │
│ 引用 task_struct.pid 的指令 │
│ 把 100 改成 108 │
│ │
└─────────────────────────────────────────────────────────────┘
實際查看 BTF:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c | grep -A5 "struct task_struct {"
struct task_struct {
struct thread_info thread_info; // offset 0
unsigned int __state; // offset 24
...
pid_t pid; // offset 1224 ← 這台機器上的真實 offset
pid_t tgid; // offset 1228
...
};
CO-RE 能處理的不只是 offset 變化
┌─────────────────────────────────────────────────────────────────┐
│ CO-RE Relocation 能處理的情況: │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ① 欄位 offset 改變(最常見) │ │
│ │ 編譯時 pid 在 +100,目標機器在 +108 │ │
│ │ → 自動修補 offset │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ② 欄位大小改變 │ │
│ │ 編譯時 pid 是 u32,目標機器改成 u64 │ │
│ │ → 自動修補讀取寬度 (4 bytes → 8 bytes) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ③ 欄位是否存在(搭配 bpf_core_field_exists) │ │
│ │ 「這個 kernel 版本有沒有這個欄位?」 │ │
│ │ → 程式可以寫 if/else 處理兩種情況 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ④ 型別是否存在(搭配 bpf_core_type_exists) │ │
│ │ 「這個 kernel 版本有沒有這個 struct?」 │ │
│ │ → 程式可以針對不同 kernel 版本走不同邏輯 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ⑤ enum 值改變 │ │
│ │ 某個 enum 常數從 3 變成 5 │ │
│ │ → 自動修補比較值 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 白話:CO-RE 不只修補地址,還能修補大小、判斷欄位存不存在、 │
│ 甚至讓程式自動適應不同 kernel 版本的結構差異。 │
│ 它是一套完整的「跨版本適配系統」。 │
└─────────────────────────────────────────────────────────────────┘
vmlinux.h:CO-RE 的起點
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 傳統做法:#include <linux/sched.h> (需要安裝 kernel-headers) │
│ │
│ CO-RE 做法:#include "vmlinux.h" (自帶!不需要 headers) │
│ │
│ vmlinux.h 是什麼? │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ $ bpftool btf dump file /sys/kernel/btf/vmlinux │ │
│ │ format c > vmlinux.h │ │
│ │ │ │
│ │ 把 kernel 的 BTF 轉成 C header 檔案 │ │
│ │ 裡面包含 kernel 所有的 struct/union/enum 定義 │ │
│ │ 通常有 10 萬行以上 │ │
│ │ │ │
│ │ 白話:「把 kernel 的零件清單翻譯成 C 語言」 │ │
│ │ │ │
│ │ 重點:這個 vmlinux.h 是從「開發機器」的 kernel 產生的 │ │
│ │ 目標機器的 kernel 可能不同 → 所以需要 CO-RE 修補 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 開發流程: │
│ │
│ 開發機 (kernel 5.4) 目標機 (kernel 5.15) │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ bpftool btf dump │ │ │ │
│ │ → vmlinux.h │ │ /sys/kernel/btf/ │ │
│ │ │ │ vmlinux │ │
│ │ 寫 .bpf.c │ │ (kernel 自帶) │ │
│ │ #include vmlinux │ │ │ │
│ │ │ 複製 .o │ libbpf 載入 │ │
│ │ clang 編譯 │────────────>│ 比對 BTF │ │
│ │ → .bpf.o │ │ 修補 offset │ │
│ └─────────────────┘ │ 載入 kernel 執行 │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
最終理解:「編譯一次,到處跑」的精確含義
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 「編譯一次」= │
│ clang 把 C → eBPF bytecode,只做一次 │
│ 產出的 .bpf.o 檔案可以複製到任何機器 │
│ │
│ 「到處跑」≠ │
│ bytecode 直接就能跑(這是 Java 的做法) │
│ │
│ 「到處跑」= │
│ libbpf 在每台機器上「載入時自動修補」bytecode │
│ 根據該機器的 kernel BTF 修正所有 struct offset │
│ 修補後的 bytecode 才送進 kernel 執行 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 精確的說法應該是: │ │
│ │ │ │
│ │ Compile Once, │ │
│ │ Patch-at-Load-Time, ← 這步是 CO-RE 偷偷幫你做的 │ │
│ │ Run Everywhere │ │
│ │ │ │
│ │ 「編譯一次,載入時自動修補,到處都能跑」 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 類比: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Java: 寫信用「世界語」→ 翻譯機(JVM)到處都能讀 │ │
│ │ │ │
│ │ CO-RE: 寫信用「中文」→ 信裡夾了一張標記紙 │ │
│ │ 標記紙寫著「第 3 行的地址要查一下收件人最新住址」 │ │
│ │ 郵差(libbpf)送信前先查地址,改好再送出 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
安全性模型:eBPF vs 傳統 Kernel Module
┌─────────────────────────────┬─────────────────────────────┐
│ 傳統 Kernel Module │ eBPF 程式 │
├─────────────────────────────┼─────────────────────────────┤
│ │ │
│ ❌ 完全信任 │ ✅ 零信任 │
│ 載入後可以做任何事 │ 每行程式碼都被 Verifier 檢查│
│ │ │
│ ❌ 可能造成 kernel panic │ ✅ 不可能 panic │
│ 一個 bug 就整台當機 │ 驗證不過就不給執行 │
│ │ │
│ ❌ 可能有記憶體洩漏 │ ✅ 自動資源管理 │
│ 忘記 free 就完蛋 │ 程式結束自動清理 │
│ │ │
│ ❌ 可能被利用來攻擊 │ ✅ 受限的功能 │
│ rootkit 常用手段 │ 只能用白名單 helper │
│ │ │
│ ❌ 需要重新編譯 kernel │ ✅ 動態載入卸載 │
│ 或至少載入 .ko 檔 │ 隨時掛上、隨時拆掉 │
│ │ │
│ 白話:請一個陌生人 │ 白話:請一個人在 │
│ 住進你家,他想幹嘛 │ 透明玻璃房裡工作, │
│ 就幹嘛 │ 看得到但出不來 │
│ │ │
└─────────────────────────────┴─────────────────────────────┘
實際應用場景
┌──────────────────────────────────────────────────────────────┐
│ eBPF 的實際應用場景 │
│ │
│ 🔍 觀測 (Observability) │
│ ├── Cilium Hubble - Kubernetes 網路流量觀測 │
│ ├── Pixie - 自動追蹤 HTTP/gRPC/SQL 請求 │
│ ├── Parca - 持續性 CPU profiling │
│ └── bpftrace - 即時系統追蹤(本 repo 有詳細介紹) │
│ │
│ 🌐 網路 (Networking) │
│ ├── Cilium - Kubernetes CNI,取代 iptables │
│ ├── Katran (Meta) - L4 負載均衡器,Facebook 用的 │
│ ├── Cloudflare - DDoS 防禦,XDP 直接在網卡擋 │
│ └── Android - 網路流量統計和防火牆 │
│ │
│ 🔐 安全 (Security) │
│ ├── Falco - 容器運行時安全監控 │
│ ├── Tetragon - 安全觀測和執行策略 │
│ └── Tracee (Aqua) - 偵測可疑系統行為 │
│ │
│ ⚡ 效能 (Performance) │
│ ├── Netflix - 全系統效能分析(Brendan Gregg 的工作) │
│ ├── Meta - 生產環境持續 profiling │
│ └── Google - 資料中心網路優化 │
│ │
└──────────────────────────────────────────────────────────────┘
總結:一張圖看懂 eBPF
┌──────────────────────────────────────────────────────────────────┐
│ │
│ USER SPACE │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ bpftrace │ │ BCC │ │ libbpf │ ← 開發工具 │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ bpf() 系統呼叫 │ ← 唯一入口 │
│ └────────┬───────┘ │
│ ═══════════════════════╪═════════════════════════════════════════ │
│ │ KERNEL SPACE │
│ ▼ │
│ ┌────────────────┐ │
│ │ Verifier │ ← 安全檢查 │
│ └────────┬───────┘ │
│ │ ✓ │
│ ▼ │
│ ┌────────────────┐ │
│ │ JIT Compiler │ ← 編譯成原生碼 │
│ └────────┬───────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ kprobe │ │ XDP │ │ trace- │ ← Hook Points │
│ │ uprobe │ │ TC │ │ point │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ eBPF Maps │ ← 資料共享 │
│ │ Ring Buffer │ │
│ └────────────────┘ │
│ │ │
│ ═════════════════════╪═══════════════════════════════════════════ │
│ │ │
│ ▼ USER SPACE │
│ ┌────────────────┐ │
│ │ 你的程式讀取 │ ← 取得結果 │
│ │ 統計/事件資料 │ │
│ └────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
核心概念回顧:
| 概念 | 白話一句話 |
|---|---|
| eBPF | 在 kernel 裡安全地跑你的小程式 |
| Verifier | 機場安檢,不安全的程式進不去 |
| JIT | 同步口譯,讓程式跑得跟 native 一樣快 |
| Maps | kernel 裡的共用白板,雙方都能讀寫 |
| Hook Point | 程式被「釘」在 kernel 的哪個事件上 |
| Helper | kernel 准你用的工具箱(白名單) |
| BTF | kernel 的零件清單(struct 長什麼樣) |
| CO-RE | 寫一次到處跑(自動適應不同 kernel) |
| bpf() syscall | user space 跟 kernel 溝通的唯一窗口 |
eBPF 為什麼開銷低?——與 utrace/ptrace 的對比
核心原因:JIT 編譯直接執行於 Kernel Space
eBPF 程式會被編譯成原生機器碼(native machine code),直接在 kernel 內執行,不需要:
- 切換到 user space
- 系統呼叫的 context switch
- 解釋執行的 overhead
傳統 utrace / ptrace 的高開銷路徑
傳統的 utrace 或基於 ptrace 的工具(如 strace)開銷極大,原因是每次事件都需要多次 context switch:
程式執行
→ 觸發 trap
→ 切換到 kernel space
→ 通知 tracer process(user space)
→ tracer 處理
→ 切換回 kernel
→ 回到被追蹤程式
每次事件 = 多次 context switch,成本極高
eBPF 的執行路徑
程式執行
→ 觸發 probe(kprobe / uprobe)
→ 直接執行 eBPF JIT 機器碼(在 kernel 內)
→ 結束,繼續執行
沒有 user space 往返,沒有 context switch
其他讓 eBPF 開銷低的設計機制
| 機制 | 說明 |
|---|---|
| Verifier | 程式載入時靜態驗證安全性,執行時無需額外檢查 |
| BPF Maps | 高效的 kernel 內資料結構,避免頻繁拷貝資料到 user space |
| Per-CPU Maps | 避免 lock contention,每個 CPU 有獨立資料副本 |
| Tail Calls | 類似 function call,但避免 stack 增長 |
| Ring Buffer | 批次傳遞資料給 user space,減少中斷次數 |
具體數字感受
用 strace 追蹤程式 → overhead 可能高達 10x ~ 100x 慢
用 eBPF(如 bpftrace)→ 通常只有 1% ~ 5% 的額外開銷
為什麼 bpftrace 能記錄 User Space 函數流程?
eBPF 不是運行在 kernel space 嗎?怎麼能追蹤 user space 的函數?
關鍵機制:uprobes
eBPF 透過 uprobe(user-space probe) 來探測 user space 函數。
1. 插入斷點指令
當你用 bpftrace 追蹤某個 user space 函數時,kernel 會在目標函數的第一條指令替換成 int3(x86 軟體中斷):
原始指令: 55 48 89 e5 (push rbp; mov rbp, rsp)
插入後: CC 48 89 e5 (int3; mov rbp, rsp)
2. 觸發流程
user space 程式執行到該函數
→ 執行到 int3
→ CPU 觸發 trap,強制進入 kernel
→ kernel 的 uprobe handler 被呼叫
→ 執行掛載在這裡的 eBPF 程式(在 kernel space)
→ eBPF 程式收集資料(stack trace、參數、時間戳等)
→ 恢復原始指令,回到 user space 繼續執行
3. 為什麼能讀取 User Space 的資料?
eBPF 程式雖然跑在 kernel space,但此時:
- context 還是原本的 process——kernel 知道是哪個 process 觸發的
- kernel 可以透過
bpf_probe_read_user()安全地讀取該 process 的記憶體 - registers(如
rdi,rsi)還保留著函數的參數值
// bpftrace 內部類似這樣讀取參數
bpf_probe_read_user(&arg0, sizeof(arg0), (void *)PT_REGS_PARM1(ctx));
4. 完整架構圖
User Space Kernel Space
──────────────────────────────────────────────────
myapp::foo()
│
└─ int3 trap ──────────────→ uprobe handler
│
└─ eBPF program 執行
│ 讀 registers
│ bpf_probe_read_user()
│ 寫入 BPF Map / ring buffer
↓
←── 恢復原始指令,繼續執行 ──────────────
bpftrace process
(user space, 非同步讀取 BPF Map)
5. 符號解析怎麼做?
bpftrace 在載入階段(不是執行時):
- 解析 ELF binary 或 DWARF debug info
- 找到函數名稱對應的記憶體位址(offset)
- 告訴 kernel 在哪個位址插入 uprobe
所以你寫 uprobe:/usr/bin/bash:readline 時,bpftrace 會先查出 readline 的 offset,再讓 kernel 在那裡插斷點。
6. uprobe 從符號解析到斷點插入的完整示意圖
你輸入的 bpftrace 命令
┌──────────────────────────────────────────────────────────┐
│ bpftrace -e 'uprobe:/usr/bin/bash:readline { ... }' │
└──────────────┬───────────────────────────────────────────┘
│
│ 解析命令,拆出三個欄位:
│ probe 類型 = uprobe
│ 目標檔案 = /usr/bin/bash
│ 函數名稱 = readline
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 1:讀取 ELF 檔頭 │
│ │
│ /usr/bin/bash (ELF binary) │
│ ┌──────────────────────────────────────────────┐ │
│ │ ELF Header │ │
│ │ Type: ET_DYN (shared object / PIE) │ │
│ │ Entry: 0x31e60 │ │
│ ├──────────────────────────────────────────────┤ │
│ │ .dynsym (動態符號表) │ │
│ │ ┌────────────────────────────────────┐ │ │
│ │ │ readline → offset 0x0b2a40 │ ◄─── 找到! │
│ │ │ main → offset 0x02e7c0 │ │ │
│ │ │ execute_cmd → offset 0x04a1e0 │ │ │
│ │ │ ... │ │ │
│ │ └────────────────────────────────────┘ │ │
│ ├──────────────────────────────────────────────┤ │
│ │ .symtab (靜態符號表,如果有的話) │ │
│ ├──────────────────────────────────────────────┤ │
│ │ .debug_info (DWARF,如果有的話) │ │
│ │ → 可取得參數型別、原始碼行號等 │ │
│ └──────────────────────────────────────────────┘ │
└──────────────┬───────────────────────────────────────────┘
│
│ 得到 readline 的 file offset = 0x0b2a40
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 2:透過 perf_event_open() / bpf() 告訴 kernel │
│ │
│ bpftrace → kernel: │
│ 「請在 /usr/bin/bash 的 offset 0x0b2a40 │
│ 插入一個 uprobe,觸發時執行我的 eBPF 程式」 │
│ │
│ 傳遞的資訊: │
│ ┌─────────────────────────────────────┐ │
│ │ target = "/usr/bin/bash" │ │
│ │ offset = 0x0b2a40 │ │
│ │ bpf_fd = <eBPF 程式的 fd> │ │
│ └─────────────────────────────────────┘ │
└──────────────┬───────────────────────────────────────────┘
│
▼
═══════════════════════════════════════════ KERNEL SPACE ═══
│
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 3:Kernel 修改目標程式的記憶體 │
│ │
│ 找到所有正在執行 /usr/bin/bash 的 process, │
│ 在虛擬位址對應的位置修改指令: │
│ │
│ bash process (PID 1234) 的記憶體: │
│ │
│ 位址 修改前 修改後 │
│ ───────────────────────────────────────────── │
│ 0x5555_55b2a40: 55 → CC (int3) │
│ 0x5555_55b2a41: 48 89 e5 48 89 e5 │
│ 0x5555_55b2a44: 41 57 41 57 │
│ ~~ ~~ │
│ push rbp int3 (trap!) │
│ mov rbp,rsp mov rbp,rsp │
│ push r15 push r15 │
│ │
│ Kernel 同時記住原始指令 0x55,以便後續恢復 │
└──────────────┬───────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ STEP 4:程式執行到 readline() 時觸發 │
│ │
│ bash process 正常執行... │
│ │ │
│ ▼ │
│ call readline() │
│ │ │
│ ▼ 碰到 0xCC (int3) │
│ ┌────────────────────────────────┐ │
│ │ CPU 觸發 #BP 異常 (trap) │ │
│ │ → 控制權交給 kernel │ │
│ └─────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ kernel uprobe handler │ │
│ │ 1. 保存 registers 快照 │ │
│ │ 2. 執行 eBPF 程式 │ ← JIT 機器碼 │
│ │ ├─ 讀 rdi (第1個參數) │ │
│ │ ├─ 讀 rsi (第2個參數) │ │
│ │ ├─ ktime_get_ns() 時間戳 │ │
│ │ └─ 寫入 ring buffer │ │
│ │ 3. 恢復原始指令 (0x55) │ │
│ │ 4. 單步執行原始指令 │ │
│ │ 5. 重新插入 int3 │ │
│ │ 6. 返回 user space │ │
│ └─────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ bash 繼續執行 readline() 的後續指令 │
│ (完全不知道剛才被攔截過) │
└──────────────────────────────────────────────────────────┘
同時在另一側...
┌──────────────────────────────────────────────────────────┐
│ bpftrace process (user space) │
│ │
│ 持續從 ring buffer 讀取事件 │
│ │ │
│ ▼ │
│ 輸出:readline 被呼叫了! │
│ timestamp=1679012345.123456 │
│ pid=1234 comm=bash │
└──────────────────────────────────────────────────────────┘
時間軸摘要:
bpftrace 啟動時(一次性):
ELF 解析 → 取得 offset → perf_event_open() → kernel 插入 int3
每次函數被呼叫時(重複):
int3 trap → uprobe handler → 執行 eBPF → 恢復指令 → 單步執行 → 重插 int3 → 返回
(整個過程 < 1 微秒)
bpftrace 結束時(一次性):
移除 uprobe → kernel 恢復所有原始指令 → 一切回到原狀
總結
| 問題 | 答案 |
|---|---|
| 誰插斷點? | Kernel(透過 uprobe 機制) |
| eBPF 跑在哪? | Kernel space |
| 怎麼讀 user space 資料? | bpf_probe_read_user() + 當下的 process context |
| user space 程式知道嗎? | 不知道,完全透明執行 |
常見誤解與澄清
誤解 ①:「eBPF 零開銷」
┌─────────────────────────────────────────────────────────────────┐
│ ❌ 誤解:「eBPF 沒有額外開銷」 │
│ ✅ 事實:eBPF 有開銷,但開銷遠低於傳統工具。不同 probe 開銷差異很大 │
└─────────────────────────────────────────────────────────────────┘
各種 probe 類型的開銷比較(單次觸發):
┌──────────────┬──────────────┬──────────────────────────────────────┐
│ Probe 類型 │ 單次開銷 │ 為什麼? │
├──────────────┼──────────────┼──────────────────────────────────────┤
│ │ │ │
│ fentry │ ~5-20 ns │ 直接在函式入口 patch call 指令 │
│ /fexit │ (最快) │ 不需要 trap,不需要單步執行 │
│ │ │ 類似 function hooking │
│ │ │ │
│ tracepoint │ ~20-80 ns │ 靜態定義的觀測點,有 overhead 但穩定 │
│ │ │ kernel 編譯時就決定了位置 │
│ │ │ │
│ kprobe │ ~50-150 ns │ 動態插 int3 在 kernel 函式 │
│ │ │ trap → handler → 單步 → 恢復 │
│ │ │ 跟 uprobe 機制相同,但在 kernel space │
│ │ │ │
│ uprobe │ ~1-5 μs │ int3 trap + user↔kernel 切換 │
│ (最慢) │ (比 kprobe │ + 單步執行 + 重插斷點 │
│ │ 慢 10x+) │ 每次觸發都涉及 page fault 級別的開銷 │
│ │ │ │
└──────────────┴──────────────┴──────────────────────────────────────┘
視覺化比較(對數刻度):
fentry ██ ~10 ns
tracepoint ████ ~50 ns
kprobe ████████ ~100 ns
uprobe ████████████████████████ ~2000 ns (2 μs)
strace ████████████████████████████████████████ ~50,000 ns (50 μs)
┌─────────────────────────────────────────────────────────────────┐
│ 重點: │
│ │
│ • uprobe 追蹤 user space 函數,每次觸發約 1-5 微秒 │
│ → 如果函數每秒被呼叫 100 萬次,overhead 就是 1-5 秒/秒 │
│ → 對高頻函數(如 malloc)要非常小心! │
│ │
│ • fentry 追蹤 kernel 函數,每次只需 ~10 奈秒 │
│ → 幾乎感覺不到 │
│ │
│ • 「eBPF 只有 1-5% overhead」這句話指的是 │
│ 整體系統效能影響,不是單次 probe 的開銷 │
│ 前提是你不要掛太多 probe 或追蹤太高頻的函數 │
└─────────────────────────────────────────────────────────────────┘
誤解 ②:「eBPF 可以做任何事」
┌─────────────────────────────────────────────────────────────────┐
│ ❌ 誤解:「eBPF 跑在 kernel 裡面,所以什麼都能做」 │
│ ✅ 事實:eBPF 是被嚴格限制的,很多事它做不到 │
└─────────────────────────────────────────────────────────────────┘
eBPF 的真實限制:
┌────────────────────┬─────────────────────────────────────────────┐
│ 限制 │ 原因與影響 │
├────────────────────┼─────────────────────────────────────────────┤
│ │ │
│ Stack 只有 │ 不能宣告大的 local 變數 │
│ 512 bytes │ char buf[1024] 直接被 Verifier 拒絕 │
│ │ → 解法:用 BPF Map 或 per-cpu array 當暫存 │
│ │ │
│ 不能 sleep │ bpf 程式在中斷/軟中斷上下文執行 │
│ │ 不能呼叫任何可能 block 的函式 │
│ │ 不能 mutex_lock、不能 kmalloc(GFP_KERNEL) │
│ │ │
│ 迴圈必須有界 │ Verifier 必須在載入時證明迴圈會終止 │
│ (Linux < 5.3 │ 早期版本完全不允許迴圈 │
│ 完全禁止迴圈) │ 5.3+ 允許 bounded loop(有上限的迴圈) │
│ │ │
│ 只能呼叫 │ 不能呼叫任意 kernel function │
│ Helper functions │ 只能用 Verifier 允許的白名單 helper │
│ │ (Linux 5.13+ 有 kfunc 放寬了一些) │
│ │ │
│ 不能直接分配 │ 沒有 malloc / free │
│ 記憶體 │ 所有記憶體必須事先在 Map 裡定義好 │
│ │ │
│ 指令數上限 │ 單一程式最多 100 萬條指令 (5.2+) │
│ │ Verifier 要走過每條可能路徑,太複雜會超時 │
│ │ │
│ 不能修改 │ 不能改 kernel 的資料結構(除了特定場景) │
│ kernel 狀態 │ 只有 XDP/TC 可以改封包,LSM 可以做安全決策 │
│ │ 追蹤類的 eBPF 是「唯讀」的 │
│ │ │
└────────────────────┴─────────────────────────────────────────────┘
對比 Kernel Module:
┌────────────────────────┬──────────────┬──────────────┐
│ 能力 │ eBPF │ Kernel Module│
├────────────────────────┼──────────────┼──────────────┤
│ 分配記憶體 │ ❌ │ ✅ │
│ Sleep / Block │ ❌ │ ✅ │
│ 呼叫任意 kernel func │ ❌ │ ✅ │
│ 建立 /proc 檔案 │ ❌ │ ✅ │
│ 註冊新的 syscall │ ❌ │ ✅ │
│ 註冊裝置驅動 │ ❌ │ ✅ │
│ 無限迴圈 │ ❌ │ ✅(但會當機) │
│ 安全性保證 │ ✅ Verifier │ ❌ 全憑自覺 │
│ 動態載入卸載 │ ✅ │ ✅ │
│ 不會 kernel panic │ ✅ │ ❌ │
└────────────────────────┴──────────────┴──────────────┘
誤解 ③:「Verifier 會幫你抓 bug」
┌─────────────────────────────────────────────────────────────────┐
│ ❌ 誤解:「程式通過 Verifier 就代表程式是正確的」 │
│ ✅ 事實:Verifier 保證安全性,不保證正確性 │
└─────────────────────────────────────────────────────────────────┘
Verifier 做的事: Verifier 不做的事:
┌──────────────────────────┐ ┌──────────────────────────┐
│ ✅ 保證程式會終止 │ │ ❌ 不檢查邏輯是否正確 │
│ ✅ 保證不會非法存取記憶體 │ │ ❌ 不檢查你讀的欄位對不對│
│ ✅ 保證不會搞壞 kernel │ │ ❌ 不檢查 Map 的值合不合理│
│ ✅ 保證用的 helper 合法 │ │ ❌ 不幫你 debug 邏輯錯誤 │
│ ✅ 保證 stack 不會溢出 │ │ ❌ 不保證你追蹤的是對的 │
└──────────────────────────┘ └──────────────────────────┘
白話:
┌──────────────────────────────────────────────────────────────┐
│ Verifier = 飛機起飛前的安全檢查 │
│ │
│ 「機翼有鎖好嗎?」 ✅ │
│ 「油有加滿嗎?」 ✅ │
│ 「引擎能啟動嗎?」 ✅ │
│ │
│ 但它不會問你: │
│ 「你飛的方向對嗎?」 ← 這是你的問題 │
│ 「你該去紐約還是倫敦?」 ← Verifier 不管目的地 │
└──────────────────────────────────────────────────────────────┘
誤解 ④:「uprobe 的 offset 是虛擬位址」
┌─────────────────────────────────────────────────────────────────┐
│ ❌ 誤解:「uprobe 用的是程式在記憶體中的虛擬位址」 │
│ ✅ 事實:uprobe 用的是 ELF 檔案內的 file offset │
│ kernel 自己處理 ASLR 和 PIE 的位址轉換 │
└─────────────────────────────────────────────────────────────────┘
現代程式都啟用 PIE(Position Independent Executable)+ ASLR:
每次執行時,程式被載入到隨機的虛擬位址。
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ELF 檔案(磁碟上): │
│ ┌──────────────────────────────────────────┐ │
│ │ .text section │ │
│ │ │ │
│ │ file offset 0x0b2a40: readline 函式 │ ← 這是固定的 │
│ │ file offset 0x02e7c0: main 函式 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 第一次執行(PID 1234): 第二次執行(PID 5678): │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ 基底: 0x5555_5500_0000│ │ 基底: 0x7f12_3400_0000│ │
│ │ │ │ │ │
│ │ readline 在虛擬位址 │ │ readline 在虛擬位址 │ │
│ │ 0x5555_550b_2a40 │ │ 0x7f12_340b_2a40 │ │
│ │ │ │ │ │
│ │ ↑ 每次不同! │ │ ↑ 每次不同! │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ 但 file offset 永遠是 0x0b2a40 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ uprobe 的註冊方式: │ │
│ │ │ │
│ │ kernel 記錄的是: │ │
│ │ inode = /usr/bin/bash 的 inode 編號 │ │
│ │ offset = 0x0b2a40(檔案內偏移) │ │
│ │ │ │
│ │ 不是記虛擬位址! │ │
│ │ │ │
│ │ 當任何 process 載入這個 inode 的檔案時, │ │
│ │ kernel 自動計算: │ │
│ │ 虛擬位址 = 載入基底 + file offset │ │
│ │ 然後在正確的虛擬位址插入 int3 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 這就是為什麼 uprobe 能自動追蹤: │
│ • 所有正在跑的 bash process(不管載入位址是多少) │
│ • 未來新開的 bash process(kernel 在 mmap 時自動插 probe) │
│ • 完全不受 ASLR 影響 │
│ │
└─────────────────────────────────────────────────────────────────┘
誤解 ⑤:「kprobe 和 tracepoint 差不多」
┌─────────────────────────────────────────────────────────────────┐
│ ❌ 誤解:「kprobe 和 tracepoint 都能追蹤 kernel,用哪個都行」 │
│ ✅ 事實:穩定性、效能、能力差很多,選錯會踩坑 │
└─────────────────────────────────────────────────────────────────┘
Probe 選型決策圖:
你要追蹤什麼?
│
├── User Space 程式的函數
│ │
│ └──→ uprobe / uretprobe
│ (唯一選擇,沒有替代方案)
│
└── Kernel 的函數
│
├── 這個函數有 tracepoint 嗎?
│ │
│ ├── 有 → 優先用 tracepoint ✅
│ │ • 穩定 API,kernel 升級不會壞
│ │ • 效能最好(靜態定義)
│ │ • 有明確的參數格式
│ │
│ └── 沒有 → 你的 kernel ≥ 5.5 嗎?
│ │
│ ├── 是 → 優先用 fentry/fexit ✅
│ │ • 比 kprobe 快 5-10x
│ │ • 直接存取函數參數(型別安全)
│ │ • 不需要 int3 trap
│ │
│ └── 否 → 用 kprobe/kretprobe
│ • 最靈活,任何函數都能掛
│ • 但不穩定:kernel 改了函數名
│ 或 inline 了,probe 就壞了
│ • 效能中等
穩定性對比:
┌──────────────┬──────────┬───────────────────────────────────────┐
│ 類型 │ 穩定性 │ kernel 升級時會發生什麼? │
├──────────────┼──────────┼───────────────────────────────────────┤
│ tracepoint │ ⭐⭐⭐ │ 幾乎不會壞。kernel 開發者承諾維護 │
│ │ 最穩定 │ 這些觀測點的介面不變 │
│ │ │ │
│ fentry │ ⭐⭐ │ 函數被 rename/inline/刪除就會壞 │
│ │ 中等 │ 但比 kprobe 好,因為有型別檢查 │
│ │ │ │
│ kprobe │ ⭐ │ 最脆弱。函數 rename、參數順序改變、 │
│ │ 最脆弱 │ 被 inline 優化掉,都會導致 probe 失效 │
└──────────────┴──────────┴───────────────────────────────────────┘
效能對比(追蹤同一個 kernel 函數):
tracepoint ██████ ~30 ns
fentry ████████ ~15 ns (最快,但需 5.5+)
kprobe ████████████████████ ~100 ns
▲
│
為什麼 fentry 比 tracepoint 還快?
因為 fentry 直接 patch function prologue,
而 tracepoint 有一層間接呼叫。
但 tracepoint 贏在穩定性。
誤解 ⑥:「eBPF 程式離開就消失了」
┌─────────────────────────────────────────────────────────────────┐
│ ❌ 誤解:「eBPF 程式只在載入它的 process 活著的時候才存在」 │
│ ✅ 事實:eBPF 程式的生命週期由 reference count 控制, │
│ 可以 pin 到 bpffs 讓它永久存活 │
└─────────────────────────────────────────────────────────────────┘
eBPF 程式的生命週期:
情境 A:正常情況(最常見)
┌──────────────────────────────────────────────────────┐
│ bpftrace 啟動 │
│ → 載入 eBPF 程式,取得 fd │
│ → 掛載到 hook point │
│ → 持續收集資料... │
│ │
│ bpftrace 結束(Ctrl+C) │
│ → fd 被 close │
│ → reference count 歸零 │
│ → kernel 自動卸載 eBPF 程式 │
│ → 恢復所有 probe 點 │
│ → 清理所有 Map │
│ │
│ 白話:人走燈滅,自動收拾 ✅ │
└──────────────────────────────────────────────────────┘
情境 B:Pin 到 bpffs(持久化)
┌──────────────────────────────────────────────────────┐
│ 載入程式 │
│ → bpf(BPF_OBJ_PIN, fd, "/sys/fs/bpf/my_prog") │
│ → 程式被「釘」在 bpffs 檔案系統上 │
│ │
│ 載入的 process 結束 │
│ → eBPF 程式繼續跑!因為 bpffs 持有 reference │
│ │
│ 清理方式: │
│ rm /sys/fs/bpf/my_prog │
│ → reference count 歸零 │
│ → 程式才會被卸載 │
│ │
│ 用途:Cilium、Calico 等網路工具用這個方式 │
│ 讓 eBPF 網路策略在 agent 重啟後仍然生效 │
└──────────────────────────────────────────────────────┘
誤解 ⑦:「bpf_probe_read 和 bpf_probe_read_user 一樣」
┌─────────────────────────────────────────────────────────────────┐
│ ❌ 誤解:「都是讀記憶體,用哪個都行」 │
│ ✅ 事實:讀錯空間會拿到垃圾資料或直接失敗 │
└─────────────────────────────────────────────────────────────────┘
三個 helper 的正確用法:
┌──────────────────────────┬──────────────────────────────────────┐
│ Helper │ 用途 │
├──────────────────────────┼──────────────────────────────────────┤
│ │ │
│ bpf_probe_read_kernel() │ 讀 kernel space 的記憶體 │
│ │ 用在 kprobe/tracepoint 裡讀 │
│ │ kernel struct 的內容 │
│ │ │
│ bpf_probe_read_user() │ 讀 user space 的記憶體 │
│ │ 用在 uprobe 裡讀 user space │
│ │ 程式的變數/參數/字串 │
│ │ │
│ bpf_probe_read() │ 舊版 API,自動猜測 kernel/user │
│ (已棄用) │ Linux 5.5+ 請改用上面兩個 │
│ │ 因為在某些架構上「猜」會猜錯 │
│ │ │
└──────────────────────────┴──────────────────────────────────────┘
為什麼要區分?
┌─────────────────────────────────────────────────────────────┐
│ │
│ 在 x86_64 上,kernel 和 user 共用同一個位址空間 │
│ 但有不同的 page table 權限 │
│ │
│ Virtual Address Space (x86_64): │
│ │
│ 0xFFFF_FFFF_FFFF_FFFF ┐ │
│ │ Kernel Space │
│ 0xFFFF_8000_0000_0000 ┘ ← 用 bpf_probe_read_kernel() │
│ │
│ ──── 巨大的未映射空洞 ──── │
│ │
│ 0x0000_7FFF_FFFF_FFFF ┐ │
│ │ User Space │
│ 0x0000_0000_0000_0000 ┘ ← 用 bpf_probe_read_user() │
│ │
│ 用錯 helper 的後果: │
│ • 最好情況:回傳 -EFAULT,讀到全 0 │
│ • 最壞情況:在某些架構(如 ARM)讀到錯誤的資料 │
│ 而且不會報錯,導致 debug 困難 │
│ │
└─────────────────────────────────────────────────────────────┘