Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Mapskernel 裡的一塊共用白板雙向讀寫:kernel 寫統計、user 讀統計
Ring Bufferkernel 裝了一台監視器,即時串流給你看單向推送: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 程式上追蹤應用程式的函式呼叫
tracepointkernel 開發者預先埋好的「官方觀測點」穩定 API,不怕 kernel 升級壞掉
fentry/fexitkprobe 的進化版,更快更安全新版 kernel 推薦用這個
XDP網路封包一進網卡就攔截(最早的攔截點)超高速封包過濾/轉發 (DDoS 防禦)
TC網路封包在 Traffic Control 層攔截流量整形、負載均衡
perf_eventCPU 硬體效能計數器觸發CPU profiling、cache miss 分析
LSMLinux 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 一樣快
Mapskernel 裡的共用白板,雙方都能讀寫
Hook Point程式被「釘」在 kernel 的哪個事件上
Helperkernel 准你用的工具箱(白名單)
BTFkernel 的零件清單(struct 長什麼樣)
CO-RE寫一次到處跑(自動適應不同 kernel)
bpf() syscalluser 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 在載入階段(不是執行時):

  1. 解析 ELF binaryDWARF debug info
  2. 找到函數名稱對應的記憶體位址(offset)
  3. 告訴 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 困難                    │
  │                                                             │
  └─────────────────────────────────────────────────────────────┘