在本文中,我們學習了很多關於 eBPF 的知識,並看到了它在各種應用中的使用範例。但如果你想根據 eBPF 實作自己的想法呢?接下來讓我們討論編寫自己的 eBPF 程式碼的選項。
eBPF 程式設計由兩部分組成:
- 編寫在核心中執行的 eBPF 程式
- 編寫管理和與 eBPF 程式互動的使用者空間程式碼
大多數函式庫和語言都要求程式設計師處理這兩部分,並瞭解在哪裡處理什麼。但 bpftrace 可能是最簡單的 eBPF 程式設計工具,它將這兩部分結合在一個簡單的介面中。
選擇 eBPF 開發工具的考量因素
在選擇 eBPF 開發工具時,需要考慮以下因素:
- 開發難度:有些工具提供更簡單的介面,但可能限制了彈性
- 效能需求:某些應用場景需要最大化效能,可能需要使用更底層的工具
- 維護成本:更高階的工具通常更容易維護,但可能無法實作所有功能
- 團隊熟悉度:選擇團隊已熟悉的程式語言通常可以加速開發
在我的實踐中,對於快速原型設計和簡單的追蹤任務,bpftrace 是絕佳選擇;而對於需要高度客製化和效能最佳化的生產環境工具,則可能需要使用 libbpf 或 BCC 等更底層的框架。
eBPF 在安全領域的應用已經從低階別的系統呼叫檢查演進到使用 eBPF 程式進行安全策略檢查、核心內事件過濾和執行時強制執行的更複雜用途。
將 eBPF 用於安全目的領域仍在積極發展中。我相信我們將看到這一領域的工具在未來幾年內不斷發展並被廣泛採用。特別是主動防禦能力,將從目前的網路安全領域擴充套件到系統安全的各個方面,為組織提供更全面、更精確的安全保障。
隨著系統安全威脅的不斷演化,eBPF 提供的深度可觀測性和即時回應能力,將成為現代安全架構中不可或缺的組成部分。安全工程師應該積極探索 eBPF 的這些能力,並將其整合到現有的安全架構中,以構建更強大的防禦系統。
bpftrace:高階 eBPF 追蹤利器
當我們談論 Linux 系統追蹤工具時,bpftrace 無疑是近年來最受矚目的工具之一。作為一種高階追蹤語言,bpftrace 讓開發者能夠輕鬆利用 eBPF 的強大功能,而不必深入瞭解底層實作細節。
bpftrace 的本質與定位
bpftrace 的設計靈感來自 awk 和 C 語言,以及像 DTrace 和 SystemTap 這樣的前輩追蹤工具。它最大的優勢在於提供了一個簡潔的高階語法,自動將這些指令轉換為 eBPF 核心程式碼,並在終端機中格式化輸出結果。
作為使用者,我們不必關注核心空間和使用者空間的分界 - bpftrace 已經為我們處理好這些細節。這種抽象讓系統追蹤變得異常簡單,即使對於那些不熟悉 eBPF 內部運作機制的開發者也是如此。
探索 bpftrace 的功能範疇
bpftrace 的功能從簡單的「Hello World」指令碼到複雜的核心資料結構追蹤都能勝任。若想快速瞭解 bpftrace 的功能範圍,可以參考 Brendan Gregg 的 bpftrace 速查表,或閱讀他的著作《BPF Performance Tools》,該書探討了 bpftrace 和 BCC 工具集。
追蹤點與事件掛鉤
顧名思義,bpftrace 可以附加到各種追蹤事件(也稱為 perf 相關事件),包括 kprobes、uprobes 和 tracepoints。我們可以使用 -l
選項列出系統上可用的追蹤點和 kprobes:
$ bpftrace -l "*execve*"
tracepoint:syscalls:sys_enter_execve
tracepoint:syscalls:sys_exit_execve
...
kprobe:do_execve_file
kprobe:do_execve
kprobe:__ia32_sys_execve
kprobe:__x64_sys_execve
...
這個指令找出所有包含 “execve” 的可用附加點。從輸出可以看出,我們可以附加到名為 do_execve 的 kprobe。下面是一個附加到該事件的 bpftrace 單行指令碼:
bpftrace -e 'kprobe:do_execve { @[comm] = count(); }'
Attaching 1 probe...
^C
@[node]: 6
@[sh]: 6
@[cpuUsage.sh]: 18
這個簡單的指令做了什麼?{ @[comm] = count(); }
部分是附加到 kprobe 事件的指令碼。這裡 comm
是當前執行程式的名稱,而 @
表示建立一個統計圖表。整個指令碼的功能是計算不同程式觸發 do_execve 事件的次數。例如,結果顯示 node 程式觸發了 6 次,cpuUsage.sh 指令碼觸發了 18 次。
多事件協作的強大指令碼
bpftrace 指令碼可以協調多個 eBPF 程式附加到不同事件。以 opensnoop.bt 指令碼為例,它報告正在被開啟的檔案:
tracepoint:syscalls:sys_enter_open,
tracepoint:syscalls:sys_enter_openat
{
@filename[tid] = args->filename;
}
tracepoint:syscalls:sys_exit_open,
tracepoint:syscalls:sys_exit_openat
/@filename[tid]/
{
$ret = args->ret;
$fd = $ret > 0 ? $ret : -1;
$errno = $ret > 0 ? 0 : - $ret;
printf("%-6d %-16s %4d %3d %s\n", pid, comm, $fd,
$errno,
str(@filename[tid]));
delete(@filename[tid]);
}
這個指令碼定義了兩個不同的 eBPF 程式,分別附加到四個不同的核心追蹤點:open() 和 openat() 系統呼叫的進入點和結束點。
第一部分程式在系統呼叫進入時觸發,它會快取檔名,將其儲存在一個對映表中,以當前執行緒 ID 作為鍵值。
第二部分程式在系統呼叫結束時觸發,它透過 /@filename[tid]/
行從對映表中檢索快取的檔名。這種設計允許我們追蹤系統呼叫的完整生命週期,從進入到結束。
執行這個指令碼會產生如下輸出:
./opensnoop.bt
Attaching 6 probes...
Tracing open syscalls... Hit Ctrl-C to end.
PID COMM FD ERR PATH
297388 node 30 0 /home/liz/.vscode-server/data/User/workspaceStorage/73ace3ed015
297360 node 23 0 /proc/307224/cmdline
297360 node 23 0 /proc/305897/cmdline
297360 node 23 0 /proc/307224/cmdline
這裡有一個有趣的細節:雖然我們只定義了四個 eBPF 程式附加到追蹤點,但輸出顯示有六個探針。這是因為完整版本的程式還包含了 BEGIN 和 END 子句(類別似於 awk 語言),用於初始化和清理指令碼。
探索 bpftrace 背後的 eBPF 程式
如果你熟悉 eBPF 的基本概念,可能會好奇 bpftrace 指令碼背後實際載入了哪些程式和對映表。使用 bpftool 可以輕鬆檢視這些資訊:
$ bpftool prog list
...
494: tracepoint name sys_enter_open tag 6f08c3c150c4ce6e gpl
loaded_at 2022-11-18T12:44:05+0000 uid 0
xlated 128B jited 93B memlock 4096B map_ids 254
495: tracepoint name sys_enter_opena tag 26c093d1d907ce74 gpl
loaded_at 2022-11-18T12:44:05+0000 uid 0
xlated 128B jited 93B memlock 4096B map_ids 254
496: tracepoint name sys_exit_open tag 0484b911472301f7 gpl
loaded_at 2022-11-18T12:44:05+0000 uid 0
xlated 936B jited 565B memlock 4096B map_ids 254,255
497: tracepoint name sys_exit_openat tag 0484b911472301f7 gpl
loaded_at 2022-11-18T12:44:05+0000 uid 0
xlated 936B jited 565B memlock 4096B map_ids 254,255
$ bpftool map list
254: hash flags 0x0
key 8B value 8B max_entries 4096 memlock 331776B
255: perf_event_array name printf flags 0x0
key 4B value 4B max_entries 2 memlock 4096B
從輸出中我們可以清楚地看到四個追蹤點程式,以及用於快取檔名的雜湊對映表(254)和用於將輸出資料從核心傳遞到使用者空間的 perf_event_array(255)。這展示了 bpftrace 如何在背後使用 eBPF 機制來實作其功能。
值得注意的是,bpftrace 工具是建構在 BCC(BPF Compiler Collection)之上的。bpftrace 指令碼會被轉換為 BCC 程式,然後在執行時使用 LLVM/Clang 工具鏈進行編譯。
eBPF 開發的語言選擇
雖然 bpftrace 提供了強大的追蹤功能,但它並不能釋放 eBPF 的全部潛力。要充分發揮 eBPF 的能力,我們需要直接為核心編寫 eBPF 程式,並處理使用者空間部分。這兩部分可以(而與經常)使用完全不同的程式語言編寫。
核心端 eBPF 程式的語言選擇
eBPF 程式可以直接用 eBPF 位元組碼編寫,但實際上,大多數程式是從 C 或 Rust 編譯成位元組碼的。這些語言的編譯器支援將 eBPF 位元組碼作為目標輸出。
值得注意的是,eBPF 位元組碼並不適合所有編譯語言。如果語言涉及執行時元件(如 Go 或 Java 的虛擬機器),它很可能與 eBPF 的驗證器不相容。例如,很難想像記憶體垃圾回收如何與驗證器對記憶體安全使用的檢查協同工作。同樣,eBPF 程式被要求是單執行緒的,因此語言中的任何並發功能都無法使用。
雖然不是真正的 eBPF,但有一個名為 XDPLua 的有趣專案,提議使用 Lua 指令碼直接在核心內編寫 XDP 程式。然而,該專案的初步研究表明,eBPF 可能更具效能優勢。隨著 eBPF 在每個核心版本中變得更強大(例如,現在能夠實作迴圈),除了個人偏好外,使用 Lua 指令碼編寫程式碼似乎沒有太多優勢。
語言選擇的實際考量
我猜測,大多數選擇使用 Rust 編寫 eBPF 核心程式碼的人也會選擇同一語言編寫使用者空間程式碼,因為分享資料結構不需要重寫。不過,這不是強制性的 - 你可以混合搭配 eBPF 程式碼和任何你選擇的使用者空間語言。
選擇用 C 語言編寫核心端程式碼的開發者也可以選擇用 C 編寫使用者空間程式碼(在這篇文章中已經看到了很多這樣的例子)。但 C 是一種相當低階的語言,需要程式設計師自行處理許多細節,尤其是記憶體管理。雖然有些人對此感到舒適,但許多人更願意用另一種更高階的語言編寫使用者空間程式碼。
無論你偏好哪種語言,你都會希望有一個提供 eBPF 支援的函式庫,這樣你就不必直接編寫系統呼叫介面。在實際開發中,這些函式庫大簡化了 eBPF 程式的開發過程。
語言選擇的深層考量
在選擇 eBPF 開發語言時,有幾個關鍵因素需要考慮:
效能與控制
C 語言提供了對系統資源的最直接控制,這在效能關鍵的場景中至關重要。當我開發需要極致效能的 eBPF 應用時,C 語言通常是我的首選,因為它允許精確控制記憶體分配和資料結構。
Rust 則提供了接近 C 的效能,同時增加了記憶體安全保證。在我的實踐中,Rust 的所有權模型在防止常見的記憶體錯誤方面表現出色,特別是在複雜的 eBPF 應用程式中。
開發效率與維護性
高階語言在使用者空間部分可以顯著提高開發效率。例如,Python 與 BCC 的組合允許快速原型設計和開發,特別適合工具和指令碼開發。
在維護大型 eBPF 專案時,我發現語言的型別系統和錯誤處理機制至關重要。Rust 的強型別系統和明確的錯誤處理在長期維護方面提供了優勢,而 Go 的簡潔語法和優秀的並發模型則適合構建複雜的使用者空間應用程式。
生態系統與社群支援
選擇語言時,現有的 eBPF 生態系統支援也是一個重要考量。目前,C 語言擁有最成熟的 eBPF 工具鏈和函式庫支援,而 Rust 的生態系統正在快速成長,特別是在安全關鍵應用領域。
深入思考:eBPF 開發未來趨勢
隨著 eBPF 技術的不斷演進,我認為未來幾年開發工具和語言支援將朝著以下方向發展:
更高階的抽象層:類別似 bpftrace 的高階抽象工具將繼續發展,使更多開發者能夠利用 eBPF 的強大功能,而無需深入瞭解底層細節。
跨語言支援增強:我們可能會看到更好的跨語言支援,允許在不同語言間更無縫地分享 eBPF 程式和資料結構。
專用 DSL 的發展:針對特定領域的 eBPF 專用語言可能會出現,類別似於 bpftrace 但針對網路、安全或其他特定用途進行了最佳化。
與雲原生工具的整合:eBPF 開發工具將更緊密地與 Kubernetes、服務網格和其他雲原生技術整合,簡化在這些環境中的佈署和管理。
eBPF 技術的魅力在於它的多功能性和低開銷,而選擇適合的開發語言和工具可以讓我們充分發揮這種技術的潛力。無論你選擇使用 bpftrace 進行快速追蹤,還是直接用 C 或 Rust 編寫核心程式,理解不同方法的優缺點都是成功開發 eBPF 應用的關鍵。
在系統可觀測性、網路和安全領域,eBPF 已經證明瞭其價值,而隨著工具和語言支援的不斷成熟,我們可以期待看到更多創新的 eBPF 應用出現在各種場景中。
eBPF 程式開發框架與語言選擇
在系統效能分析和網路最佳化領域,eBPF 已成為 Linux 核心中不可或缺的技術。然而,要有效開發 eBPF 程式,選擇合適的開發框架和程式語言至關重要。本文將探討各種 eBPF 開發選項,幫助開發者根據專案需求做出最佳選擇。
BCC 框架:Python、Lua 和 C++ 的選擇
BCC (BPF Compiler Collection) 是開發 eBPF 工具最流行的框架之一,它支援多種程式語言來撰寫使用者空間的程式碼,最常見的是 Python。
Python 與 BCC 的結合
BCC 的 Python 繫結提供了一種相對簡單的方式來開發 eBPF 工具。以下是一個簡單的 BCC 範例:
#!/usr/bin/python3
from bcc import BPF
program = """
BPF_RINGBUF_OUTPUT(output, 1);
int hello(void *ctx) {
// eBPF 程式碼...
output.ringbuf_output(&data, sizeof(data), 0);
return 0;
}
"""
b = BPF(text=program)
# 其他設定...
b["output"].open_ring_buffer(print_event)
# 主迴圈...
這段程式碼展示了 BCC 的核心工作方式。在 Python 程式中,我們定義了一個字串變數 program
,包含 C 語言風格的 eBPF 程式碼。BCC 框架在執行時會進行以下處理:
- 將
program
字串進行預處理,擴充套件像BPF_RINGBUF_OUTPUT
這樣的巨集 - 將看似物件導向的語法 (如
output.ringbuf_output()
) 轉換為底層的 BPF 輔助函式呼叫 (如bpf_ringbuf_output()
) - 使用 Clang 編譯器在執行時編譯處理後的程式碼
- 將編譯好的 eBPF 位元碼載入核心
BCC 的一個獨特之處在於它能夠同時為核心空間和使用者空間定義分享結構。例如,BPF_RINGBUF_OUTPUT(output, 1)
這一行不僅在核心端定義了環形緩衝區,也讓使用者空間程式能透過 b["output"]
存取它。
BCC 的其他語言支援
除了 Python 外,BCC 也支援使用 Lua 和 C++ 來開發使用者空間程式碼。在 BCC 專案的 examples
目錄中有 lua
和 cpp
子目錄,提供了這些語言的範例程式碼。不過,相較於 Python 範例,這些語言的檔案和範例較少,對於初學者可能不那麼友善。
BCC 的優勢與限制
BCC 的主要優勢在於它的易用性和豐富的工具集。它為初學者提供了一個相對容易的入門方式,特別是對於已熟悉 Python 的開發者。
然而,BCC 也有其限制:
- 執行時編譯:每次執行工具時都需要編譯 eBPF 程式碼,這會導致啟動延遲
- 記憶體佔用:需要攜帶編譯工具鏈,增加了記憶體佔用
- 分發複雜性:分發 BCC 工具需要在目標系統上安裝編譯器和相關依賴
根據這些原因,如果要開發生產級的、需要廣泛分發的 eBPF 工具,玄貓建議考慮本文後續討論的其他框架。
C 語言與 libbpf
C 是開發 eBPF 程式最原始也最強大的語言選擇。大多數 eBPF 程式都是用 C 語言撰寫,然後使用 LLVM/Clang 編譯為 eBPF 位元碼。
libbpf 的優勢
libbpf 是一個 C 語言函式庫,提供了載入和管理 eBPF 程式的 API。它支援 CO-RE (Compile Once – Run Everywhere) 技術,使得 eBPF 程式能夠在不同版本的 Linux 核心上執行,而無需重新編譯。
相較於 BCC,使用 libbpf 開發的工具有以下優勢:
- 較小的記憶體佔用:不需要在執行時攜帶編譯器工具鏈
- 更快的啟動時間:無需執行時編譯,工具可以立即啟動
- 簡化的分發:編譯好的工具可以直接在目標系統上執行,無需額外依賴
事實上,BCC 專案本身也在逐步將其工具重寫為根據 libbpf 的版本,這些重寫版本通常被認為是更好的選擇。
libxdp:XDP 程式開發的輔助函式庫
對於開發 XDP (eXpress Data Path) 程式的開發者,libxdp 是一個建立在 libbpf 之上的專門函式庫,它簡化了 XDP 程式的開發和管理。這是 xdp-tools 專案的一部分,該專案還包含了 XDP Tutorial,這是學習 eBPF 程式設計的絕佳資源。
C 語言的挑戰
雖然 C 語言強大與靈活,但它也是一種相對低階的語言,需要開發者自行管理記憶體和處理緩衝區。這容易導致安全漏洞和指標誤用造成的程式當機。
eBPF 驗證器在核心端提供了一定的保護,但使用者空間的 C 程式碼並沒有類別似的保護機制。因此,對於不熟悉 C 語言的開發者來說,使用其他高階語言的 eBPF 開發框架可能是更好的選擇。
Go 語言的 eBPF 開發選項
Go 語言在基礎設施和雲原生工具開發中被廣泛採用,因此它也提供了多種 eBPF 開發選項。
gobpf:早期的 Go 實作
gobpf 是最早的 Go 語言 eBPF 函式庫之一,它是 Iovisor 專案的一部分,與 BCC 並列。然而,這個專案已經很長時間沒有積極維護,目前正在討論將其棄用。因此,在選擇 Go 語言 eBPF 函式庫時,應該考慮其他更活躍的選項。
ebpf-go:Cilium 的 Go 實作
Cilium 專案中的 eBPF Go 函式庫 (cilium/ebpf) 是目前最受歡迎的 Go 語言 eBPF 開發選項之一。它提供了純 Go 實作的 eBPF 程式和對映管理功能,包括 CO-RE 支援。
使用 cilium/ebpf 函式庫,開發者可以:
- 使用提供的
bpf2go
工具將 eBPF 程式編譯為位元碼,並嵌入到 Go 原始碼中 - 載入和管理獨立的 ELF 檔案形式的 eBPF 程式 (類別似於本文中看到的 *.bpf.o 範例)
cilium/ebpf 函式庫支援多種 eBPF 程式型別,包括:
- 用於追蹤的 perf 事件 (包括較新的 fentry 事件)
- XDP 和 cgroup socket 附加點等網路程式型別
在 cilium/ebpf 專案的 examples 目錄中,C 語言的核心程式和對應的 Go 使用者空間程式碼放在同一個目錄下,C 檔案以 // +build ignore
開頭,這告訴 Go 編譯器忽略這些檔案。
以下是一個簡化的 cilium/ebpf 使用範例:
package main
import (
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
)
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf my_program.c
func main() {
// 載入 eBPF 程式
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
// 錯誤處理
}
defer objs.Close()
// 連結程式到鉤子點
kp, err := link.Kprobe("sys_execve", objs.MyProgram, nil)
if err != nil {
// 錯誤處理
}
defer kp.Close()
// 主要邏輯...
}
這個 Go 範例展示瞭如何使用 cilium/ebpf 函式庫載入和管理 eBPF 程式。關鍵步驟包括:
- 使用
bpf2go
工具將 C 語言的 eBPF 程式編譯並嵌入到 Go 程式中 - 在執行時載入這些已編譯的 eBPF 物件
- 將 eBPF 程式連結到適當的鉤子點 (在此例中是系統呼叫
sys_execve
) - 實作主要的使用者空間邏輯
這種方式的優點是最終產生一個單一的 Go 二進位檔,其中包含了 eBPF 位元碼,可以在不同的核心上執行,而無需任何除了 Linux 核心本身以外的依賴。
Rust 語言的 eBPF 開發
Rust 因其安全性和效能特性而受到系統程式設計者的青睞,它也提供了幾個用於 eBPF 開發的函式庫。
libbpf-rs
libbpf-rs 是 libbpf 的 Rust 繫結,它提供了一種安全與符合 Rust 風格的方式來使用 libbpf 的功能。這個函式庫是由 Facebook 開發和維護的,並且被用於生產環境中。
使用 libbpf-rs 的一個主要優勢是它與 libbpf 緊密整合,這意味著它能夠快速支援 libbpf 中的新功能。
Aya
Aya 是一個純 Rust 實作的 eBPF 函式庫,它提供了從 Rust 開發 eBPF 程式的端對端解決方案。與其他需要使用 C 語言編寫核心端程式的函式庫不同,Aya 允許開發者使用 Rust 編寫整個 eBPF 應用程式,包括核心端和使用者空間部分。
Aya 的設計理念是提供一個符合 Rust 慣例的 API,同時利用 Rust 的型別系統來提供額外的安全保證。它還包含了一個名為 aya-log 的子專案,用於從 eBPF 程式中記錄日誌。
其他語言選項
除了上述主要語言外,還有其他語言的 eBPF 開發選項:
C# 和 .NET
對於 .NET 開發者,Bindings.NET 提供了 libbpf 的 .NET 繫結。這使得 C# 開發者能夠開發 eBPF 應用程式,雖然核心端程式仍需使用 C 語言編寫。
Java
對於 Java 開發者,有幾個選項可用:
- jbpf:一個純 Java 實作的 eBPF 函式庫
- bpf-linker:由 Parca 專案提供的一個工具,用於將 eBPF 程式與 Java 應用程式連結
高階語言的 eBPF 開發
對於那些偏好更高階、更抽象的開發方式的開發者,有一些專門的 eBPF 開發語言:
- bpftrace:一種高階追蹤語言,類別似於 DTrace,專為 eBPF 設計
- Bumblebee:一個根據設定的方法,使用 YAML 描述 eBPF 程式的行為
框架選擇考量因素
選擇合適的 eBPF 開發框架和語言時,需要考慮以下因素:
開發效率與學習曲線
- BCC + Python:對 Python 開發者友好,學習曲線較平緩,但不適合生產環境分發
- C + libbpf:功能最完整,控制最精細,但學習曲線較陡峭
- Go/Rust 函式庫:平衡了開發效率和生產就緒性,適合已熟悉這些語言的開發者
佈署與分發需求
- 執行時編譯 (如 BCC):適合本地開發和測試,不適合廣泛分發
- 預編譯 + CO-RE (如 libbpf):適合生產環境和廣泛分發的工具
程式型別與目標
- 系統追蹤:幾乎所有框架都支援
- 網路程式 (XDP):C + libxdp 提供最佳支援,其次是 Go 和 Rust 函式庫
- 安全監控:C + libbpf 或 Rust 函式庫通常是最佳選擇
維護與社群支援
- 活躍開發:檢查專案的最近提交和問題解決情況
- 使用案例:尋找類別似您目標的成功實作案例
- 檔案品質:良好的檔案能大幅減少開發時間
對於初學者,玄貓建議從 BCC + Python 開始,因為它有豐富的檔案和範例。一旦熟悉了 eBPF 的基本概念,再根據專案需求考慮轉向更適合生產環境的框架,如 libbpf 或 Go/Rust 函式庫。
實際應用案例分析
讓我們透過幾個實際案例來看不同框架的適用場景:
案例一:臨時效能分析工具
需求:開發一個臨時工具來分析系統中的特定效能問題。
建議選擇:BCC + Python 或 bpftrace
理由:這類別工具通常只在特定系統上執行一次,不需要考慮分發問題。BCC 和 bpftrace 提供了快速開發的能力,特別適合一次性分析任務。
案例二:容器網路監控工具
需求:開發一個持續執行的工具,監控容器環境中的網路流量。
建議選擇:Go + cilium/ebpf 或 Rust + Aya
理由:這類別工具需要佈署到多種環境中,需要良好的可移植性和低資源消耗。Go 和 Rust 函式庫提供了良好的 CO-RE 支援,同時保持了較低的資源佔用。
案例三:核心安全監控系統
需求:開發一個高效能、低延遲的系統呼叫監控工具。
建議選擇:C + libbpf
理由:安全監控需要最大限度地控制和最小化開銷,C + libbpf 提供了最直接的方式來實作這些目標,沒有額外的語言執行時開銷。
結論與最佳實踐
在 eBPF 程式開發中,沒有一種框架或語言適合所有場景。選擇應該根據專案需求、團隊專業知識和長期維護考量。
對於大多數開發者,玄貓推薦以下路徑:
- 使用 BCC + Python 或 bpftrace 學習 eBPF 概念
- 對於生產環境工具,根據團隊專長選擇:
- 熟悉 C:使用 libbpf
- 熟悉 Go:使用 cilium/ebpf
- 熟悉 Rust:使用 libbpf-rs 或 Aya
無論選擇哪種框架,都應該關注 eBPF 技術的快速發展,定期更新知識和工具。eBPF 生態系統正在迅速發展,新的功能和改進不斷湧現,保持學習的態度對於掌握這一強大技術至關重要。
eBPF 的多樣化開發選項反映了它在現代 Linux 系統中的重要性和廣泛應用場景。透過選擇適合的開發框架和語言,開發者可以充分利用 eBPF 的強大功能,構建高效、安全的系統和網路工具。
Go 與 Rust:eBPF 開發的現代語言選擇
隨著 eBPF 技術的普及,越來越多開發者開始尋找高效、安全與開發體驗良好的語言來構建 eBPF 應用程式。在這個領域中,Go 和 Rust 兩種現代系統程式設計語言脫穎而出,各自提供了獨特的優勢和工具生態系統。
作為一個長期關注系統程式設計的技術工作者,玄貓發現這兩種語言的工具鏈已經發展得相當成熟,能夠滿足從簡單監控工具到複雜網路安全解決方案的各種需求。本文將探討這些工具鏈的特性、優勢和使用場景。
Go 語言的 eBPF 工具鏈
Go 語言以其簡潔的語法、強大的標準函式庫和優秀的併發模型贏得了基礎設施開發者的青睞。在 eBPF 開發領域,Go 提供了幾個成熟的解決方案,讓開發者能夠輕鬆地構建和佈署 eBPF 程式。
使用 cilium/ebpf 進行 Go 開發
cilium/ebpf 是目前 Go 語言中最受歡迎的 eBPF 函式庫之一,它提供了純 Go 實作的 eBPF 工具鏈,不依賴 CGo 或外部 C 函式庫,這使得它在跨平台開發和佈署上具有顯著優勢。
使用者空間檔案結構
在 cilium/ebpf 專案中,使用者空間的 Go 檔案通常包含一行特殊的指令,告訴 Go 編譯器呼叫 bpf2go 工具處理 C 檔案:
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf <C檔案名稱> -- -I../headers
這行指令使用 Go 的 go:generate
機制,在建構過程中自動執行 bpf2go 工具。這個工具負責將 C 語言編寫的 eBPF 程式編譯成位元組碼,並生成對應的 Go 程式碼框架。引數說明:
-cc $BPF_CLANG
:指定使用 CLANG 編譯器-cflags $BPF_CFLAGS
:傳遞編譯旗標bpf <C檔案名稱>
:指定輸出檔案字首和輸入的 C 原始碼檔案-- -I../headers
:傳遞給編譯器的額外引數,這裡是包含頭檔的路徑
當你在套件上執行 go generate
命令時,它會重建 eBPF 程式並在一個步驟中重新生成框架程式碼。這個過程類別似於第 5 章中介紹的 bpftool gen skeleton
,但 bpf2go 生成的是 Go 程式碼而非 C 程式碼。輸出檔案還包括包含位元組碼的 .o 物件檔。
實際上,bpf2go 會生成兩個版本的位元組碼 .o 檔案,分別對應大端序和小端序架構。同時也會生成兩個相應的 .go 檔案,在編譯時會自動選用正確的版本。
以 cilium/ebpf 中的 kprobe 範例為例,自動生成的檔案包括:
bpf_bpfeb.o
和bpf_bpfel.o
:包含 eBPF 位元組碼的 ELF 檔案bpf_bpfeb.go
和bpf_bpfel.go
:定義對應這些位元組碼中的 maps、programs 和 links 的 Go 結構和函式
自動生成的 Go 程式碼結構
自動生成的 Go 程式碼與原始 C 程式碼有直接的對應關係。以 kprobe 範例中的 C 程式碼為例:
struct bpf_map_def SEC("maps") kprobe_map = {
...
};
SEC("kprobe/sys_execve")
int kprobe_execve() {
...
}
自動生成的 Go 程式碼包括代表所有 maps 和 programs 的結構(在這個例子中,各只有一個):
type bpfMaps struct {
KprobeMap *ebpf.Map `ebpf:"kprobe_map"`
}
type bpfPrograms struct {
KprobeExecve *ebpf.Program `ebpf:"kprobe_execve"`
}
這些 Go 結構體直接對映到 C 程式碼中定義的 eBPF maps 和 programs。名稱 “KprobeMap” 和 “KprobeExecve” 是從 C 程式碼中使用的 map 和 program 名稱派生而來,並按照 Go 的命名慣例進行了調整(首字母大寫)。
這些物件被組合到一個 bpfObjects
結構中,代表所有要載入到核心的內容:
type bpfObjects struct {
bpfPrograms
bpfMaps
}
在使用者空間 Go 程式碼中使用自動生成的物件
有了這些物件定義和相關的自動生成函式,你就可以在使用者空間 Go 程式碼中使用它們。以下是根據同一個 kprobe 範例中 main 函式的摘錄(為了簡潔,省略了錯誤處理):
objs := bpfObjects{}
loadBpfObjects(&objs, nil)
defer objs.Close()
kp, _ := link.Kprobe("sys_execve", objs.KprobeExecve, nil)
defer kp.Close()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
var value uint64
objs.KprobeMap.Lookup(mapKey, &value)
log.Printf("%s called %d times\n", fn, value)
}
這段程式碼展示了使用自動生成的框架程式碼的典型流程:
- 首先建立
bpfObjects
例項,並使用loadBpfObjects
函式將嵌入在位元組碼形式中的所有 BPF 物件載入到核心中 - 使用
link.Kprobe
將程式附加到sys_execve
kprobe 上 - 設定一個計時器,使程式碼每秒輪詢一次 map
- 從 map 中讀取專案,並輸出執行次數
cilium/ebpf 目錄中還有其他幾個範例,可以作為參考和靈感來源。
使用 libbpfgo 進行 Go 開發
libbpfgo 是由 Aqua Security 開發的專案,它實作了一個圍繞 libbpf C 程式碼的 Go 封裝,提供了載入和附加程式的工具,以及使用 Go 原生功能如通道來接收事件。由於它建立在 libbpf 之上,因此支援 CO-RE(Compile Once, Run Everywhere)。
以下是摘自 libbpfgo README 的範例,它提供了對這個函式庫的高層次概述:
bpfModule := bpf.NewModuleFromFile(bpfObjectPath)
bpfModule.BPFLoadObject()
mymap, _ := bpfModule.GetMap("mymap")
mymap.Update(key, value)
rb, _ := bpfModule.InitRingBuffer("events", eventsChannel, buffSize)
rb.Start()
e := <-eventsChannel
這段程式碼示範了 libbpfgo 的基本用法:
- 從物件檔案讀取 eBPF 位元組碼
- 將該位元組碼載入到核心
- 操作 eBPF map 中的條目
- 設定 ring buffer 接收事件
Go 程式設計師會欣賞在通道上接收來自 ring buffer 或 perf buffer 的資料的方式,這是專為處理非同步事件設計的語言特性。
這個函式庫最初是為 Aqua 的 Tracee 安全專案建立的,現在也被其他專案使用,如 Polar Signals 的 Parca,它提供根據 eBPF 的 CPU 分析。使用這個專案的唯一顧慮是 libbpf C 程式碼和 Go 之間的 CGo 邊界,可能會導致效能和其他問題。
Rust 語言的 eBPF 工具鏈
雖然 Go 在過去十年左右的時間裡一直是許多基礎設施程式設計的既定語言,但最近越來越多的開發者更傾向於使用 Rust。
Rust 越來越多地被用於構建基礎設施工具。它允許像 C 一樣的低階存取,但具有記憶體安全的額外好處。事實上,Linus Torvalds 在 2022 年確認 Linux 核心本身將開始納入 Rust 程式碼,最近的 6.1 版本已經有了一些初步的 Rust 支援。
正如我在本章前面討論的,Rust 可以編譯成 eBPF 位元組碼,這意味著(在適當的函式庫支援下)可以用 Rust 編寫 eBPF 工具的使用者空間和核心程式碼。
Rust eBPF 開發有幾個選擇:libbpf-rs、Redbpf 和 Aya。
libbpf-rs:Rust 的 libbpf 封裝
libbpf-rs 是 libbpf 專案的一部分,它提供了圍繞 libbpf C 程式碼的 Rust 封裝,讓你可以用 Rust 編寫 eBPF 程式碼的使用者空間部分。從專案的範例可以看出,eBPF 程式本身是用 C 語言編寫的。
值得注意的是,libbpf-bootstrap 專案中還有更多 Rust 範例,旨在幫助你嘗試使用這個 crate 構建自己的程式碼。
這個 crate 有助於將 eBPF 程式納入根據 Rust 的專案,但它不能滿足許多人希望用 Rust 編寫核心端程式碼的願望。讓我們看其他能夠實作這一點的專案。
Redbpf:完整的 Rust eBPF 框架
Redbpf 是一組與 libbpf 介面的 Rust crates,作為 foniod(一個根據 eBPF 的安全監控代理)的一部分開發。
Redbpf 早於 Rust 編譯到 eBPF 位元組碼的能力,因此它使用多步驟編譯過程,包括從 Rust 編譯到 LLVM 位元組碼,然後使用 LLVM 工具鏈生成 ELF 格式的 eBPF 位元組碼。Redbpf 支援多種程式型別,包括 tracepoints、kprobes 和 uprobes、XDP、TC 和一些 socket 事件。
隨著 Rust 編譯器 rustc 獲得直接生成 eBPF 位元組碼的能力,這被名為 Aya 的專案所利用。按照 ebpf.io 社群網站的說法,Aya 被認為是"新興的",而 Redbpf 被列為主要專案,但從個人角度來看,勢頭似乎正在向 Aya 移動。
Aya:純 Rust 實作的 eBPF 框架
Aya 是直接在系統呼叫層面用 Rust 構建的,所以它不依賴於 libbpf(實際上也不依賴於 BCC 或 LLVM 工具鏈)。但它確實支援 BTF 格式,支援與 libbpf 相同的重定位(如第 5 章所述),所以它提供了相同的 CO-RE 能力,可以編譯一次並在其他核心上執行。在撰寫本文時,它支援比 Redbpf 更廣泛的 eBPF 程式型別,包括追蹤/效能相關事件、XDP 和 TC、cgroups 和 LSM 附件。
如前所述,Rust 編譯器也支援編譯到 eBPF 位元組碼,因此這種語言可以用於核心和使用者空間的 eBPF 程式設計。
不依賴 LLVM 中間層而能夠在 Rust 中原生編寫核心端和使用者空間端的能力吸引了 Rust 程式設計師選擇這個選項。GitHub 上有一個有趣的討論,關於 lockc 專案(一個根據 eBPF 的專案,使用 LSM 鉤子增強容器工作負載的安全性)的開發者為何決定將其專案從 libbpf-rs 移植到 Aya。
該專案包括 aya-tool,一個用於生成與核心資料結構比對的 Rust 結構定義的工具,這樣你就不必自己編寫它們。
Aya 專案強調開發者體驗,讓新手容易上手。有鑑於此,“Aya book” 是一個非常易讀的介紹,帶有一些很好的範例程式碼,並附有用的解釋。
為了讓你簡單瞭解 Rust 中的 eBPF 程式碼是什麼樣子,以下是 Aya 的基本 XDP 範例的摘錄,該範例允許所有流量:
#[xdp(name="myapp")]
pub fn myapp(ctx: XdpContext) -> u32 {
match unsafe { try_myapp(ctx) } {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_ABORTED,
}
}
unsafe fn try_myapp(ctx: XdpContext) -> Result<u32, u32> {
info!(&ctx, "received a packet");
Ok(xdp_action::XDP_PASS)
}
這段程式碼展示了 Rust 中 eBPF XDP 程式的基本結構:
#[xdp(name="myapp")]
定義了 section 名稱,等同於 C 語言中的SEC("xdp/myapp")
myapp
函式是主要入口點,接收一個 XDP 上下文並回傳一個 u32 值(對應於 XDP 動作)- 實際處理在
try_myapp
函式中進行,它記錄接收到的資料包並回傳XDP_PASS
動作,允許資料包繼續正常處理 - 錯誤處理使用 Rust 的 Result 型別,如果出現錯誤則回傳
XDP_ABORTED
在選擇 eBPF 開發工具鏈時,需要考慮多種因素,包括團隊的語言偏好、專案需求、效能要求和開發體驗。以下是一些建議:
Go 工具鏈的適用場景
cilium/ebpf:
- 適合希望避免 CGo 和外部依賴的純 Go 專案
- 適合需要跨平台構建和佈署的場景
- 適合已經熟悉 Go 生態系統的團隊
libbpfgo:
- 適合需要 CO-RE 功能的專案
- 適合需要與現有根據 libbpf 的程式整合的場景
- 適合能夠接受 CGo 效能開銷的專案
Rust 工具鏈的適用場景
libbpf-rs:
- 適合希望在 Rust 專案中使用 eBPF 但仍希望用 C 編寫 eBPF 程式的團隊
- 適合需要與現有 libbpf 生態系統整合的專案
Redbpf:
- 適合需要成熟、穩定的 Rust eBPF 解決方案的專案
- 適合已經投資於根據 LLVM 的編譯流程的團隊
Aya:
- 適合希望完全在 Rust 中編寫 eBPF 程式(包括核心和使用者空間部分)的團隊
- 適合重視開發者體驗的新專案
- 適合需要廣泛支援不同程式型別的專案
開發經驗與最佳實踐
在實際開發 eBPF 應用程式時,除了選擇合適的工具鏈外,還有一些最佳實踐值得注意:
充分利用自動生成的程式碼:無論是 cilium/ebpf 的 bpf2go 還是 Aya 的 aya-tool,都能大減少手動編寫範本程式碼的工作量。
注意核心版本相容性:儘管 CO-RE 技術使得跨核心版本佈署變得更加容易,但仍需要注意目標環境的核心版本和可用特性。
效能測試和最佳化:eBPF 程式直接執行在核心中,效能至關重要。確保進行充分的效能測試和最佳化。
錯誤處理和日誌記錄:在生產環境中,良好的錯誤處理和日誌記錄對於診斷和解決問題至關重要。
安全性考慮:eBPF 程式具有強大的系統存取能力,確保遵循最小許可權原則,只請求必要的許可權。
eBPF 技術和相關工具鏈正在迅速發展。一些值得關注的趨勢包括:
Rust 在系統程式設計中的崛起:隨著 Rust 被納入 Linux 核心,我們可以預期更多的系統級工具和基礎設施將採用 Rust,包括 eBPF 相關工具。
工具鏈的成熟與整合:各種 eBPF 工具鏈正在變得更加成熟,並且更廣泛的開發生態系統整合。
更高階別的抽象:我們可能會看到更多高階別的抽象和框架,使 eBPF 開發變得更加簡單和直觀。
擴充套件到更多領域:eBPF 技術正在從網路和安全擴充套件到更多領域,包括可觀測性、效能最佳化和資源管理。
Go 和 Rust 為 eBPF 開發提供了強大與靈活的工具鏈,使開發者能夠利用這些現代語言的優勢構建高效、安全的系統級應用程式。隨著 eBPF 技術的不斷發展,這些工具鏈也將繼續演進,為開發者提供更好的開發體驗和更強大的功能。
無論你選擇 Go 還是 Rust,重要的是瞭解每種工具鏈的優勢和限制,並根據專案的具體需求做出明智的選擇。希望本文能夠幫助你在這個激動人心的技術領域中找到適合的開發路徑。