Linux eBPF

最後更新: 2026-06-09 02:54:06 +0800 +0800
tags: Bpf | Kernel | Linux
原文以英文撰寫。您正在閱讀AI翻譯版本。

eBPF 是 Linux 核心中的驗證執行環境。應用程式載入程式與映射,將程式附加至核心定義的掛鉤,並透過檔案描述元交換資料。網路處理仍是重要用途,但追蹤、安全性原則與效能分析不再依賴封包擷取。

從封包過濾器到 Linux Socket 過濾器 #

這條設計脈絡始於 DEC 為 ULTRIX 和 DIGITAL UNIX 等系統提供的封包過濾器。程序開啟封包過濾裝置、選取介面、使用 ioctl() 安裝 enfilter 指令序列,並讀取接受的鏈路層封包。過濾器使用堆疊機器,而多個取用者依優先順序排列。

Steven McCanne 與 Van Jacobson 設計 Berkeley Packet Filter(BPF),以降低封包複製與解譯成本。他們 1993 年的論文描述了兩項至今仍可辨識的變更:

  • 以暫存器為基礎的虛擬機器決定是否接受封包,以及要複製多少位元組;
  • 每個開啟的 /dev/bpf 執行個體都有自己的過濾器與封包緩衝區,因此封包擷取是以裝置執行個體表示,而不是 DEC 的優先順序鏈。

傳統 BPF(現在稱為 cBPF)具有 32 位元累加器 A、索引暫存器 X、16 個暫存字、封包載入指令、條件跳躍、算術運算與回傳值。Linux 將此指令格式用於 Linux Socket Filtering(LSF),但會將過濾器直接附加到 socket,而不要求使用 /dev/bpf。

c
struct sock_filter code[] = {
    BPF_STMT(BPF_LD  | BPF_B | BPF_ABS, 9), /* IPv4 protocol byte */
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_TCP, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, 0xffff),
    BPF_STMT(BPF_RET | BPF_K, 0),
};
struct sock_fprog program = {
    .len = sizeof(code) / sizeof(code[0]),
    .filter = code,
};

setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER,
           &program, sizeof(program));

此範例假設 socket 的資料從 IPv4 標頭開始,因此第 9 個位元組是其通訊協定欄位。封包配置取決於 socket 系列與掛鉤。對於 socket 過濾器,回傳值是要保留的封包位元組數;零表示拒絕封包。核心會在安裝指令序列前檢查它。SO_LOCK_FILTER 可以防止過濾器被取代或移除,讓程序能夠附加過濾器後再放棄權限。libpcap 與 tcpdump 會將高階擷取運算式編譯成此指令模型。

eBPF 執行模型 #

Linux 現代化的 BPF 系統呼叫介面在 Linux 3.18 引入。eBPF 使用固定寬度的 64 位元指令與 11 個 64 位元暫存器:

  • R0 儲存函式或程式的回傳值。
  • R1 到 R5 傳遞引數,並由呼叫者保存。
  • R6 到 R9 由被呼叫者保存。
  • R10 是每次呼叫堆疊的唯讀框架指標。

進入時,R1 指向程式類型專屬的內容,例如 xdp_md、__sk_buff 或追蹤內容。程式回傳掛鉤專屬的動作:XDP 程式可以通過、丟棄、重新導向或傳送封包,而追蹤程式通常回傳零。

直譯器可以執行已接受的位元組碼。當架構啟用 BPF JIT 時,核心會將程式轉換為原生指令,並可根據 sysctl 原則強化產生的映像。JIT 編譯會改變執行成本,但不會改變驗證器要求。

驗證器定義安全邊界 #

程式附加前,驗證器會以符號方式執行所有可能路徑。它追蹤暫存器與堆疊槽的類型、純量範圍、指標來源、對齊、初始化狀態、參照所有權與界限。因此,只有在控制流程證明要求的位元組位於 data_end 之前後,封包讀取之類的程式碼才會被接受。

驗證器會強制執行下列屬性:

  • 不使用未初始化的暫存器或堆疊資料;
  • 不將純量任意轉換為核心指標;
  • 僅使用程式類型核准的內容欄位與輔助函式;
  • 記憶體存取有界,且指標運算有效;
  • 追蹤參照的取得與釋放保持平衡;
  • 執行有界,包括可證明會終止的迴圈。

接受程式只證明其符合驗證器模型,而不代表功能正確。有效程式仍可能丟棄每個封包、彙總錯誤的索引鍵,或增加過多額外負擔。複雜度限制也表示,當路徑探索無法建立所需不變量時,邏輯上安全的原始碼仍可能被拒絕。

映射與輔助函式 #

映射是具有特定類型索引鍵/值儲存與並行語意的核心物件。雜湊、陣列、每 CPU 映射、環形緩衝區、LRU 映射、最長前綴樹、程式陣列與儲存映射分別服務不同的通訊模式。使用者空間透過 bpf() 命令建立與操作映射,通常由 libbpf 包裝:

c
int map_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "counts",
                            sizeof(__u32), sizeof(__u64),
                            1024, NULL);
bpf_map_update_elem(map_fd, &key, &value, BPF_ANY);

程式使用核准的輔助函式或 BPF kfunc 查找映射元素、讀取時間與程序識別、重新導向封包、發出環形緩衝區事件,或存取掛鉤專屬資料。輔助函式的可用性與引數類型取決於程式類型。映射查找可能回傳 NULL;驗證器要求在解參照前進行 null 檢查。

映射是共享狀態,因此競爭仍是應用程式需要處理的問題。每 CPU 映射可避免計數器的跨 CPU 更新。只有在其文件化限制允許的情況下,才能使用原子操作或 struct bpf_spin_lock;使用者空間必須明確彙總每 CPU 值。

程式類型與附加點 #

程式類型會選取內容、回傳慣例、驗證器操作與允許的輔助函式。附加類型則進一步識別掛鉤。常見組合包括:

領域程式與附加範例
早期封包路徑網路裝置上的 XDP
流量控制TC ingress/egress 上的 SCHED_CLS 或 SCHED_ACT
Socketsocket 過濾器、reuseport 選取、socket 查找、sockops、cgroup socket 掛鉤
追蹤tracepoint、raw tracepoint、kprobe/kretprobe、fentry/fexit、perf 事件
安全性LSM 掛鉤與 cgroup 原則掛鉤

掛鉤選擇同時決定語意與成本。在受支援的驅動程式上,XDP 會在一般網路堆疊的 socket 緩衝區配置前執行,而 TC 程式會看到 skb。fentry/fexit 使用由 BTF 描述的函式邊界,而 kprobe 以指令位址為目標,因此更容易受到實作變更影響。

權限、生命週期與所有權 #

除非核心與系統原則允許所要求的操作,否則載入需要特權。現代核心可以使用 CAP_BPF、CAP_PERFMON 與 CAP_NET_ADMIN 分離權限;較舊路徑與某些操作仍使用 CAP_SYS_ADMIN。發行版核心可以透過 kernel.unprivileged_bpf_disabled 停用非特權載入,而 lockdown、LSM、命名空間、權杖或委派原則也可能施加額外限制。應檢查正在執行的核心,而不要將單一 capability 設定視為通用方案。

程式、映射、BTF 物件與連結都是透過檔案描述元公開的參照計數核心物件。關閉最後一個參照會摧毀物件。將物件固定在 BPF 檔案系統(bpffs)中,會為它提供持久路徑與參照:

bash
mount -t bpf bpf /sys/fs/bpf
bpftool prog show
bpftool map show
bpftool link show

bpf_link 將附加關係獨立於原始程式 FD 表示,並為受支援的掛鉤提供更安全的生命週期:關閉連結會解除程式附加,除非其他參照或固定項保持連結存活。固定項會在目前開機期間持續存在,即使建立程序已結束。重新開機後,啟動程式碼必須重新建立核心物件;無法重新開啟先前核心執行個體中的物件。

不依賴無限制存取的可觀測性 #

追蹤程式可以測量延遲、計算事件、取樣堆疊、關聯排程器活動,並透過環形緩衝區匯出結構化記錄。CO-RE 重定位使用 BTF,使欄位存取能夠適應相容的核心類型配置。營運工具仍應考量:

  • 取用者無法排空緩衝區時的事件遺失;
  • 高頻掛鉤上的每事件額外負擔;
  • 缺少適用 BTF 的核心上的符號與類型變更;
  • 公開核心或程序資料所造成的隱私與安全影響。

使用 bpftool prog show、bpftool prog dump xlated、驗證器記錄與程式執行時間統計資料檢查已載入的內容。可觀測性程式碼會在生產環境核心路徑中執行,因此設計必須包含有界額外負擔與明確的解除附加計畫。

參考資料 #

評論

在 GitHub 上閱讀或評論(發表評論需要登入)