瞭解了 BTF 的基本概念後,讓我們看如何在實際專案中應用 CO-RE 技術。CO-RE 的主要優勢在於它允許我們編寫一次 BPF 程式,然後在不同的核心版本上執行,而無需重新編譯。
建立 CO-RE 相容的 BPF 程式
要建立 CO-RE 相容的 BPF 程式,我們需要遵循以下步驟:
- 使用 libbpf 或其他支援 CO-RE 的函式庫
- 確保核心設定了 BTF 支援
- 使用 BPF 骨架來簡化使用者空間程式碼
以下是一個簡單的 CO-RE 相容程式範例:
// BPF 程式部分
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u32);
__type(value, u64);
} my_map SEC(".maps");
SEC("kprobe/do_sys_open")
int bpf_prog(struct pt_regs *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 count = 1;
u64 *value;
value = bpf_map_lookup_elem(&my_map, &pid);
if (value) {
count += *value;
}
bpf_map_update_elem(&my_map, &pid, &count, BPF_ANY);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
這個簡單的 BPF 程式追蹤系統中的檔案開啟操作。它使用 kprobe 附加到 do_sys_open
函式,每次有程式開啟檔案時都會執行。程式會記錄每個程式(按 PID 識別)開啟的檔案數量,並將這些訊息儲存在一個雜湊對映中。
注意程式中的 SEC 巨集,它用於定義 ELF 節,這些節在載入程式時被 libbpf 識別和處理。SEC(".maps")
定義了對映節,而 SEC("kprobe/do_sys_open")
定義了程式將附加到的探針型別和位置。
使用 BPF 骨架簡化使用者空間程式
一旦我們編譯了 BPF 程式,就可以使用 bpftool gen skeleton
生成骨架,然後在使用者空間程式中使用它:
// 使用者空間程式部分
#include <stdio.h>
#include <unistd.h>
#include "my_bpf_program.skel.h"
int main()
{
struct my_bpf_program *skel;
int err;
// 設定 libbpf 錯誤和除錯訊息回呼
libbpf_set_print(libbpf_print_fn);
// 開啟 BPF 應用
skel = my_bpf_program__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// 載入並驗證 BPF 程式
err = my_bpf_program__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
// 附加 BPF 程式
err = my_bpf_program__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
"to see output of the BPF programs.\n");
// 無限迴圈,直到按 Ctrl+C
for (;;) {
// 讀取對映資料並處理
sleep(1);
}
cleanup:
my_bpf_program__destroy(skel);
return -err;
}
這個使用者空間程式使用生成的骨架來管理 BPF 程式的生命週期。它首先開啟骨架,然後載入並驗證 BPF 程式,最後將程式附加到指定的事件。骨架提供了型別安全的 API,使得與 BPF 程式和對映的互動變得簡單。
注意使用者空間程式如何使用骨架提供的函式(如 my_bpf_program__open()
、my_bpf_program__load()
等)來管理 BPF 程式,而不是直接使用底層的 libbpf 函式。這大簡化了程式碼,並減少了錯誤的可能性。
CO-RE 重定位過程
CO-RE 的核心是重定位過程,它允許 BPF 程式適應不同核心版本中的結構佈局差異。當 BPF 程式被載入核心時,libbpf 會使用 BTF 訊息來確定程式編譯環境和執行環境之間的差異,並相應地調整程式。
這個過程包括:
- 從目標核心取得 BTF 訊息
- 比較程式編譯時的結構佈局與目標核心中的佈局
- 調整程式中的偏移量以適應目標核心
這種調整是透明的,開發者不需要手動處理這些差異,這正是 CO-RE 的美妙之處。
CO-RE 的實際案例研究
讓我們透過一個實際案例來更好地理解 CO-RE 的價值。假設我們想要開發一個監控網路通訊端活動的 BPF 程式,但我們知道不同核心版本中通訊端結構可能有差異。
不使用 CO-RE 的問題
在傳統方法中,我們可能需要:
- 為每個目標核心版本維護不同的程式碼分支
- 在執行時檢測核心版本並選擇正確的程式碼路徑
- 使用複雜的巨集和條件編譯來處理差異
這種方法不僅複雜與容易出錯,還會導致程式碼維護困難。
使用 CO-RE 的解決方案
使用 CO-RE,我們可以編寫一個單一版本的程式,它可以在所有支援的核心版本上執行:
// BPF 程式部分
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/socket.h>
#include <linux/net.h>
// 使用 CO-RE 重定位來存取通訊端結構
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(int));
__uint(value_size, sizeof(int));
} events SEC(".maps");
SEC("kprobe/sock_sendmsg")
int BPF_KPROBE(trace_sendmsg, struct socket *sock, struct msghdr *msg)
{
struct event_t {
u32 pid;
u16 family;
u16 type;
u64 ts;
} event;
// 取得程式 ID
event.pid = bpf_get_current_pid_tgid() >> 32;
event.ts = bpf_ktime_get_ns();
// 存取通訊端結構 - CO-RE 會自動處理不同核心版本的偏移量
event.family = BPF_CORE_READ(sock, sk, __sk_common.skc_family);
event.type = BPF_CORE_READ(sock, type);
// 將事件傳送到效能緩衝區
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return 0;
}
char LICENSE[] SEC("license") = "GPL";
這個程式使用 BPF_CORE_READ
巨集來存取通訊端結構中的欄位。這個巨集是 CO-RE 的關鍵部分,它告訴 libbpf 這些欄位存取需要在載入時進行重定位。
當程式在不同的核心版本上執行時,CO-RE 會自動調整對 sock->sk->__sk_common.skc_family
和 sock->type
的存取,以比對目標核心中的實際結構佈局。這使得程式能夠在不同的核心版本上正確執行,而無需任何修改或重新編譯。
注意 BPF_KPROBE
巨集的使用,它簡化了 kprobe 處理函式的定義,並自動處理引數存取。這是 libbpf 提供的另一個便利功能,使得 BPF 程式更容易編寫和理解。
CO-RE 與 BCC 的比較
BCC (BPF Compiler Collection) 是另一個流行的 BPF 開發框架,它採用不同的方法來處理核心版本差異:
- BCC 在執行時編譯 BPF 程式,使用目標系統的核心頭檔案
- CO-RE 在編譯時生成可重定位的 BPF 程式,在載入時進行調整
CO-RE 的優勢包括:
- 更快的啟動時間 - 無需在執行時編譯
- 更小的佈署包 - 無需包含核心頭檔案
- 更好的可移植性 - 一個二進位檔案可以在多個核心版本上執行
- 更好的除錯體驗 - 編譯錯誤在開發時就能發現,而不是在佈署時
這些優勢使得 CO-RE 成為開發生產級 BPF 應用的首選方法。
構建高階 CO-RE 應用的最佳實踐
在開發生產級 CO-RE 應用時,以下是一些最佳實踐:
1. 使用 libbpf-bootstrap 作為起點
libbpf-bootstrap 提供了一個完整的專案範本,包括 Makefile、骨架生成和基本的程式結構。它是開始新專案的理想起點。
2. 謹慎使用 BTF 重定位
雖然 CO-RE 可以處理大多數結構差異,但有些情況需要特別注意:
- 當欄位被完全移除或重新命名時
- 當結構的語義發生變化時
- 當存取巢狀指標結構時
在這些情況下,可能需要使用條件編譯或在執行時檢測功能可用性。
3. 處理向後相容性
為了確保程式在舊核心上也能執行,可以使用 libbpf 的功能檢測 API:
// 使用者空間程式部分
if (!bpf_probe_helper(skel->progs.my_prog, BPF_FUNC_perf_event_output)) {
fprintf(stderr, "Kernel doesn't support perf_event_output helper\n");
return 1;
}
4. 最佳化效能
CO-RE 應用的效能與傳統 BPF 應用相同,但有些技巧可以進一步最佳化:
- 使用適當的對映型別和大小
- 最小化 BPF 程式的複雜性
- 使用批處理來減少使用者空間和核心之間的互動
5. 全面的測試策略
為 CO-RE 應用開發全面的測試策略至關重要:
- 在多個核心版本上測試
- 測試邊界情況和錯誤處理
- 使用 BPF 測試框架自動化測試
BPF CO-RE 技術為開發跨核心版本的 eBPF 應用開闢了新的可能性。透過 BTF 型別訊息和人工智慧重定位,我們可以編寫一次 BPF 程式,然後在各種不同的環境中執行它。
BPF 骨架進一步簡化了開發過程,提供了型別安全的 API 來管理 BPF 程式的生命週期。結合 libbpf 和其他現代工具,CO-RE 使得開發高效、可靠和可移植的 BPF 應用變得比以往往任何時候都更容易。
隨著 Linux 核心不斷發展,CO-RE 將繼續發揮關鍵作用,使得 BPF 技術能夠在保持向後相容性的同時不斷創新。對於系統工程師和效能分析師來說,掌握 CO-RE 技術將是一項寶貴的技能,使他們能夠構建下一代的觀測性、安全性和網路工具。
理解 BTF 型別系統與其在 eBPF 中的應用
在處理 eBPF(extended Berkeley Packet Filter)程式時,需要面對一個關鍵挑戰:如何讓 eBPF 程式正確理解並操作核心資料結構。這就是 BTF(BPF Type Format)型別系統發揮作用的地方。BTF 為 eBPF 程式提供了完整的型別資訊,讓開發者能夠更容易地編寫、除錯和維護 eBPF 程式。
BTF 型別資訊的解析與應用
在 eBPF 程式中,BTF 型別資訊提供了資料結構的詳細佈局。讓我們透過一個實際例子來理解這些資訊:
[5] INT 'char' size=1 bits_offset=0 nr_bits=8 encoding=NONE
[6] ARRAY '(anon)' type_id=5 index_type_id=7 nr_elems=12
[7] INT '__ARRAY_SIZE_TYPE__' size=4 bits_offset=0 nr_bits=32 encoding=NONE
這段 BTF 資訊描述了一個陣列型別。讓我們逐行解析:
- 第一行定義了一個名為
char
的基本型別(type_id=5),大小為 1 byte,這與 C 語言中的 char 型別完全相符。 - 第二行定義了一個匿名陣列(type_id=6),其元素型別為前面定義的 char(type_id=5),索引型別為 type_id=7,與包含 12 個元素。
- 第三行定義了索引型別(type_id=7),這是一個 4 byte 的整數,用於陣列索引。
透過這些定義,我們可以完整理解記憶體中 user_msg_t
結構的佈局,這個結構佔用 12 bytes 的記憶體空間。
結構體中的欄位佈局
接下來,我們來看一個更複雜的結構體定義:
[8] STRUCT '____btf_map_config' size=16 vlen=2
'key' type_id=1 bits_offset=0
'value' type_id=4 bits_offset=32
這是一個名為 ____btf_map_config
的結構體定義,用於 eBPF map 中的鍵值對。結構體總大小為 16 bytes,包含 2 個欄位:
key
:型別為 type_id=1(u32 型別),位於結構體起始位置(bits_offset=0)value
:型別為 type_id=4(user_msg_t 結構體),從結構體起始位置偏移 32 bits(4 bytes)開始
這個結構體佈局完全符合邏輯,因為第一個 32 位元用於存放 key 欄位,之後才是 value 欄位。
C 結構體中的記憶體對齊
在 C 語言中,結構體欄位會自動進行記憶體對齊,這意味著一個欄位不一定緊接著前一個欄位。例如:
struct something {
char letter;
u64 number;
}
在這個結構中,letter
欄位後會有 7 bytes 的未使用空間,然後才是 number
欄位,這樣 64 位元的 number 可以對齊到 8 的倍數的記憶體位置。
在某些情況下可以啟用編譯器封裝選項來避免這種未使用的空間,但通常會導致效能下降。我在實際開發中發現,手動設計結構體以高效利用空間是更常見的做法。
帶有 BTF 資訊的 Maps
瞭解了 BTF 如何描述資料型別後,讓我們看這些 BTF 資料如何在建立 map 時傳遞給核心。
在第四章中,我們瞭解到 maps 是使用 bpf(BPF_MAP_CREATE)
系統呼叫建立的。這個呼叫接受一個 bpf_attr
結構作為引數,該結構在核心中定義如下(省略了一些細節):
struct { /* anonymous struct used by BPF_MAP_CREATE command */
__u32 map_type; /* one of enum bpf_map_type */
__u32 key_size; /* size of key in bytes */
__u32 value_size; /* size of value in bytes */
__u32 max_entries; /* max number of entries in a map */
...
char map_name[BPF_OBJ_NAME_LEN];
...
__u32 btf_fd; /* fd pointing to a BTF type data */
__u32 btf_key_type_id; /* BTF type_id of the key */
__u32 btf_value_type_id; /* BTF type_id of the value */
...
};
在 BTF 引入之前,bpf_attr
結構中沒有 btf_*
欄位,核心對鍵值結構沒有任何瞭解。key_size
和 value_size
欄位定義了它們需要多少記憶體,但它們只被視為一堆積積位元組。
透過額外傳入定義鍵和值型別的 BTF 資訊,核心可以檢視它們,而像 bpftool 這樣的工具可以檢索型別資訊進行美化輸出。
有趣的是,為鍵和值傳入的是分開的 BTF 型別 ID。我們剛才看到的 ____btf_map_config
結構體並不是由核心用於 map 定義的,它只是由 BCC 在使用者空間端使用。
函式和函式原型的 BTF 資料
BTF 資料不僅包含資料型別的資訊,還包含函式和函式原型的資訊。以下是描述 hello
函式的 BTF 資料:
[31] FUNC_PROTO '(anon)' ret_type_id=23 vlen=1
'ctx' type_id=10
[32] FUNC 'hello' type_id=31 linkage=static
- 型別 32 定義了名為
hello
的函式,其型別定義在前一行(type_id=31)。 - 這是一個函式原型,回傳型別為 type_id=23,接受單個引數(vlen=1)名為
ctx
,型別為 type_id=10。
對應的型別定義如下:
[10] PTR '(anon)' type_id=0
[23] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
- 型別 10 是一個匿名指標,預設型別為 0(void 指標)。
- 回傳值型別 23 是一個 4 byte 的有符號整數。
這完全對應了原始碼中的函式定義:
int hello(void *ctx)
檢查 Maps 和 Programs 的 BTF 資料
如果想檢查特定 map 關聯的 BTF 型別,bpftool
提供了簡單的方法。例如,對於 config
map:
$ bpftool btf dump map name config
[1] TYPEDEF 'u32' type_id=2
[4] STRUCT 'user_msg_t' size=12 vlen=1
'message' type_id=6 bits_offset=0
同樣,可以使用 bpftool btf dump prog <prog identity>
檢查特定程式的 BTF 資訊。
產生核心標頭檔
在支援 BTF 的核心上執行 bpftool btf list
,會看到許多預先存在的 BTF 資料塊:
$ bpftool btf list
1: name [vmlinux] size 5842973B
2: name [aes_ce_cipher] size 407B
3: name [cryptd] size 3372B
...
列表中的第一項,ID 為 1,名為 vmlinux
,包含了這台機器上執行的核心使用的所有資料型別、結構和函式定義的 BTF 資訊。
eBPF 程式需要它將參照的任何核心資料結構和型別的定義。在 CO-RE(Compile Once, Run Everywhere)出現之前,通常需要找出 Linux 核心原始碼中包含感興趣結構定義的特定標頭檔,但現在有了更簡單的方法,因為支援 BTF 的工具可以從核心中包含的 BTF 資訊產生適當的標頭檔。
這個標頭檔通常稱為 vmlinux.h
,可以使用 bpftool
產生:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
這個檔案定義了核心的所有資料型別,因此在 eBPF 程式原始碼中包含這個產生的 vmlinux.h
檔案,就能提供所需的任何 Linux 資料結構定義。當編譯原始碼為 eBPF 物件檔時,該物件將包含與此標頭檔中使用的定義相比對的 BTF 資訊。
稍後,在目標機器上執行程式時,載入它到核心的使用者空間程式將進行調整,以適應此建置時 BTF 資訊與目標機器上執行的核心的 BTF 資訊之間的差異。
以 /sys/kernel/btf/vmlinux
檔案形式的 BTF 資訊自 Linux 核心版本 5.4 起被包含在核心中,但 libbpf 可以使用的原始 BTF 資料也可以為較舊的核心產生。換句話說,如果想在沒有 BTF 資訊的目標機器上執行支援 CO-RE 的 eBPF 程式,可能可以自行提供該目標的 BTF 資料。
BTF 與 CO-RE:實作跨核心版本的相容性
BTF 資訊的最大價值之一是它支援 CO-RE(Compile Once, Run Everywhere)技術。CO-RE 允許我們編譯一次 eBPF 程式,然後在不同的 Linux 核心版本上執行,即使這些核心的資料結構有所不同。
在實際應用中,我發現 BTF 和 CO-RE 極大地簡化了 eBPF 程式的佈署流程。以前,我們需要為不同的核心版本編譯不同版本的 eBPF 程式,或者在執行時動態編譯,這增加了佈署的複雜性和風險。現在,使用 BTF 和 CO-RE,我可以編寫一個 eBPF 程式,並在各種 Linux 發行版和核心版本上佈署它。
實際案例:跨核心版本效能監控
例如,我曾經需要為一個混合雲環境開發一個統一的效能監控解決方案,其中包含從 Ubuntu 18.04(核心 4.15)到最新的 Fedora(核心 5.14)的各種 Linux 發行版。使用 BTF 和 CO-RE,我能夠開發一個單一的 eBPF 程式來收集關鍵效能指標,而不必擔心不同核心版本之間的資料結構差異。
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
__uint(max_entries, 1024);
} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_read")
int trace_read_enter(struct trace_event_raw_sys_enter *ctx)
{
u64 id = bpf_get_current_pid_tgid();
u32 pid = id >> 32;
struct event_data data = {
.pid = pid,
.fd = (int)ctx->args[0],
.timestamp = bpf_ktime_get_ns(),
};
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, sizeof(data));
return 0;
}
這個程式追蹤 read
系統呼叫的進入點。它使用 vmlinux.h
提供的核心型別定義,這使它能夠正確存取 trace_event_raw_sys_enter
結構,無論目標核心版本如何。程式收集程式 ID、檔案描述符和時間戳,並將其傳送到效能事件陣列。
由於 BTF 和 CO-RE 的支援,這個程式可以在不同的核心版本上執行,而不需要修改或重新編譯。libbpf 在執行時會處理資料結構的差異,確保程式正確存取所需的欄位。
BTF 的未來發展與實際應用
BTF 和 CO-RE 技術正在迅速發展,為 eBPF 程式設計帶來更多可能性。我預見未來會有更多工具和框架利用 BTF 資訊來簡化 eBPF 開發流程,特別是在自動化除錯和最佳化方面。
在我的實踐中,我發現 BTF 不僅對於 eBPF 程式有用,還可以作為核心內部結構的檔案工具。當我需要理解特定核心功能的實作時,我經常使用 BTF 資訊來探索相關的資料結構,這比閱讀原始碼更加高效。
BTF 和 CO-RE 的結合使 eBPF 成為跨發行版、跨版本的 Linux 可觀測性和效能最佳化解決方案的理想選擇。透過理解和利用 BTF,我們可以編寫更強大、更可移植的 eBPF 程式,從而更好地理解和最佳化 Linux 系統的行為。
在 eBPF 程式設計中,掌握 BTF 型別系統是邁向高階應用的關鍵一步。它不僅提供了對核心內部結構的深入瞭解,還使我們能夠開發出在各種 Linux 環境中可靠執行的工具。隨著 eBPF 生態系統的不斷發展,BTF 將繼續發揮核心作用,使 eBPF 技術在系統可觀測性、安全性和效能最佳化方面的應用更加廣泛和強大。
CO-RE:實作 eBPF 程式的跨核心可攜性
在前面的探討中,玄貓介紹了 BTF(BPF Type Format)的基本概念。BTF 提供了核心結構的型別資訊,這是實作 CO-RE(Compile Once, Run Everywhere)的根本。如果你對 BTF 內部運作機制感興趣,BTFHub 儲存函式庫中提供了更深入的資料可供參考。
接下來,讓我們探討如何運用 BTF 以及其他技術來撰寫可跨不同核心版本執行的 eBPF 程式。
CO-RE eBPF 程式設計
眾所周知,eBPF 程式在核心空間執行。在這個部分,玄貓將專注於核心端的程式碼設計,之後再討論如何從使用者空間與這些在核心中執行的程式互動。
如前所述,eBPF 程式需要被編譯成 eBPF 位元組碼。截至撰文時間,支援這種編譯的工具主要有用於 C 語言的 Clang 或 gcc 編譯器,以及 Rust 編譯器。雖然在第 10 章會討論使用 Rust 的選項,但在本章中,玄貓將假設你使用 C 語言和 Clang 編譯器,並搭配 libbpf 函式庫。
hello-buffer-config 範例應用
在本章剩餘部分,玄貓將以一個名為 hello-buffer-config
的範例應用程式為例。這個範例與前一章使用 BCC 框架的 hello-buffer-config.py
非常相似,但這個版本是使用 C 語言編寫,並採用 libbpf 和 CO-RE 技術。
如果你有根據 BCC 的 eBPF 程式碼想要遷移到 libbpf,Andrii Nakryiko 在其網站上提供了一份出色與全面的。BCC 提供了一些便捷的捷徑,這些在 libbpf 中的處理方式有所不同;相反地,libbpf 也提供了自己的巨集和函式庫函式,以簡化 eBPF 程式設計師的工作。在探討這個範例時,玄貓會指出 BCC 和 libbpf 方法之間的一些差異。
你可以在 github.com/lizrice/learning-ebpf 儲存函式庫的 chapter5 目錄中找到本文的 C 語言 eBPF 程式範例。
首先讓我們看 hello-buffer-config.bpf.c
,它實作了在核心中執行的 eBPF 程式。後面玄貓將展示使用者空間程式碼 hello-buffer-config.c
,這部分負責載入程式並顯示輸出,類別似於第 4 章中 BCC 實作的 Python 程式碼。
標頭檔案設計
與任何 C 程式一樣,eBPF 程式需要包含一些標頭檔案。以下是 hello-buffer-config.bpf.c
開頭幾行指定的標頭檔案:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "hello-buffer-config.h"
這五個檔案包括 vmlinux.h 檔案、來自 libbpf 的幾個標頭以及一個應用程式專屬的標頭檔案。這是 libbpf 程式典型的標頭檔案模式,讓我們來瞭解原因。
核心標頭資訊
如果你正在編寫參照任何核心資料結構或型別的 eBPF 程式,最簡單的選擇是包含前面描述的 vmlinux.h
檔案。或者,也可以從 Linux 原始碼中包含個別的標頭檔案,或者如果你真的願意費心的話,在自己的程式碼中手動定義這些型別。如果你打算使用 libbpf 的任何 BPF 輔助函式,則需要包含 vmlinux.h
或 linux/types.h
來取得 BPF 輔助函式原始碼參照的型別定義,如 u32、u64 等。
vmlinux.h
檔案是從核心原始碼標頭衍生的,但它不包括其中的 #define 值。例如,如果你的 eBPF 程式解析乙太網路封包,你可能需要告訴你封包含什麼協定的常數定義(如 0x0800 表示它是 IP 封包,或 0x0806 表示 ARP 封包)。如果你不包含定義這些值的 if_ether.h
檔案,則需要在自己的程式碼中複製一系列常數值。在 hello-buffer-config
中不需要這些值定義,但在第 8 章中會看到一個相關的例子。
來自 libbpf 的標頭
要在 eBPF 程式碼中使用任何 BPF 輔助函式,需要包含提供其定義的 libbpf 標頭檔案。
關於 libbpf 有一點可能稍微令人困惑,它不僅是一個使用者空間函式庫。你會發現自己在使用者空間和 eBPF C 程式碼中都包含來自 libbpf 的標頭檔案。
截至撰寫時,常見的做法是將 libbpf 作為子模組包含在 eBPF 專案中,並從原始碼建置/安裝—這就是玄貓在本文範例儲存函式庫中所做的。如果你將其包含為子模組,只需要從 libbpf/src 目錄執行 make install
。玄貓認為不久後,在常見的 Linux 發行版上以套件形式廣泛使用 libbpf 將會更普遍,尤其是因為 libbpf 現已發布 1.0 版本。
應用程式專屬標頭
有一個應用程式專屬的標頭檔案定義應用程式使用者空間和 eBPF 部分共用的結構是很常見的做法。在範例中,hello-buffer-config.h
標頭檔案定義了 data_t
結構,用於從 eBPF 程式傳遞事件資料到使用者空間。它與 BCC 版本中的結構幾乎相同:
struct data_t {
int pid;
int uid;
char command[16];
char message[12];
char path[16];
};
與之前版本的唯一區別是增加了一個名為 path
的欄位。
將此結構定義放入單獨標頭檔案的原因是,玄貓也會在 hello-buffer-config.c
的使用者空間程式碼中參照它。在 BCC 版本中,核心和使用者空間程式碼都定義在單一檔案中,BCC 在幕後做了一些工作使該結構可用於 Python 使用者空間程式碼。
定義對映(Maps)
在包含標頭檔案後,hello-buffer-config.bpf.c
原始碼的下一部分定義了用於對映的結構:
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} output SEC(".maps");
struct user_msg_t {
char message[12];
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, u32);
__type(value, struct user_msg_t);
} my_config SEC(".maps");
這比等效的 BCC 範例需要更多的程式碼行!在 BCC 中,名為 config
的對映是使用以下巨集建立的:
BPF_HASH(config, u64, struct user_msg_t);
當不使用 BCC 時,這個巨集不可用,所以在 C 中必須詳細寫出。可以看到玄貓使用了 __uint
和 __type
。這些定義在 bpf/bpf_helpers_def.h
中,連同 __array
,如下所示:
#define __uint(name, val) int (*name)[val]
#define __type(name, val) typeof(val) *name
#define __array(name, val) typeof(val) *name[]
這些巨集通常按慣例在根據 libbpf 的程式中使用,玄貓認為它們使對映定義更容易閱讀。
名稱 “config” 與 vmlinux.h 中的定義衝突,所以玄貓在這個範例中將對映重新命名為 “my_config”。
eBPF 程式段落
使用 libbpf 要求每個 eBPF 程式都用 SEC()
巨集標記,定義程式型別,如:
SEC("kprobe")
這在編譯的 ELF 物件中產生一個名為 kprobe 的段落,因此 libbpf 知道將其載入為 BPF_PROG_TYPE_KPROBE
。玄貓會在第 7 章中進一步討論不同的程式型別。
根據程式型別,還可以使用段落名稱指定程式將附加到的事件。libbpf 函式庫將使用此資訊自動設定附加,而不是讓你在使用者空間程式碼中明確設定。例如,要自動附加到 ARM 機器上 execve 系統呼叫的 kprobe,可以這樣指定段落:
SEC("kprobe/__arm64_sys_execve")
這需要你知道該架構上系統呼叫的函式名稱(或者找出來,也許可以檢視目標機器上的 /proc/kallsyms
檔案,它列出了所有核心符號,包括其函式名稱)。但 libbpf 可以使用 k(ret)syscall
段落名稱讓生活更輕鬆,它告訴載入器自動附加到架構特定函式的 kprobe:
SEC("ksyscall/execve")
有效的段落名稱和格式列在 libbpf 檔案中。過去,段落名稱的要求更寬鬆,所以你可能會遇到使用不同名稱格式的舊 eBPF 程式碼。
在這段程式碼中,我們看到了如何使用 SEC 巨集來指定 eBPF 程式的型別和附加點。這個巨集的作用是在編譯出的 ELF 檔案中建立特定的段落,讓 libbpf 能夠識別程式型別並正確處理。
特別值得注意的是 libbpf 提供的便捷功能,允許透過特定格式的段落名稱(如 ksyscall/execve
)自動處理架構特定的系統呼叫附加,這大簡化了跨架構的 eBPF 程式開發。這是 CO-RE 理念的一部分,讓開發者不必關心底層架構的具體差異。
撰寫 eBPF 程式
在定義了必要的標頭檔案和對映後,接下來讓我們看實際的 eBPF 程式如何編寫。以下是 hello-buffer-config.bpf.c
中的一個範例函式:
SEC("ksyscall/openat")
int BPF_PROG(hello, int dfd, const char *filename, int flags)
{
struct data_t data = {};
struct user_msg_t *p;
u32 uid;
// 僅跟蹤特定 UID 的事件
uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
data.uid = uid;
data.pid = bpf_get_current_pid_tgid() >> 32;
// 從設定對映中查詢訊息
p = bpf_map_lookup_elem(&my_config, &uid);
if (p != 0) {
bpf_probe_read_user_str(&data.message, sizeof(data.message), p->message);
} else {
bpf_probe_read_user_str(&data.message, sizeof(data.message), "default");
}
// 讀取命令名稱和檔案路徑
bpf_get_current_comm(&data.command, sizeof(data.command));
bpf_probe_read_user_str(&data.path, sizeof(data.path), filename);
// 傳送事件到效能緩衝區
bpf_perf_event_output(ctx, &output, BPF_F_CURRENT_CPU, &data, sizeof(data));
return 0;
}
這個 eBPF 程式附加到 openat
系統呼叫,每當程式嘗試開啟檔案時都會執行。讓我們分析其工作原理:
- 首先,程式使用
SEC("ksyscall/openat")
指定它應該附加到 openat 系統呼叫。 - 函式宣告使用
BPF_PROG
巨集,這是 libbpf 提供的便捷方式,自動處理上下文引數。 - 程式取得當前程式的 UID 和 PID,這些是我們想要記錄的資訊。
- 接著嘗試從
my_config
對映中查詢與當前 UID 相關聯的訊息。如果找到,則使用該訊息;否則使用 “default”。 - 然後讀取當前程式的命令名稱(使用
bpf_get_current_comm
)和被開啟的檔案路徑。 - 最後,使用
bpf_perf_event_output
將收集的資料傳送到效能緩衝區,這樣使用者空間程式就能讀取這些資訊。
這展示了 eBPF 程式如何攔截系統呼叫、收集資訊並將其傳遞到使用者空間。注意使用 bpf_probe_read_user_str
函式安全地從使用者空間讀取字串資料,這是 eBPF 程式處理使用者空間資料的標準方式。
使用 libbpf 的優勢
相比於 BCC,使用 libbpf 和 CO-RE 方法有幾個顯著優勢:
更好的效能 - libbpf 程式不需要在執行時編譯,因此啟動更快與資源使用更少。
更少的依賴 - BCC 需要安裝完整的 LLVM 和 Clang 工具鏈,而 libbpf 程式只需要預編譯的 eBPF 物件檔案。
可攜性 - 透過 CO-RE,eBPF 程式可以在不同的核心版本上執行,無需重新編譯。
更好的佈署體驗 - 預編譯的 eBPF 程式更容易封裝和佈署。
直接的核心 API 存取 - libbpf 提供更直接的 eBPF 核心 API 存取,允許更精細的控制。
然而,這些優勢是以更複雜的程式碼為代價的。正如我們所見,libbpf 需要更多的樣板程式碼來設定對映和程式。
使用者空間整合
雖然本文主要關注核心空間的 eBPF 程式,但值得簡要討論使用者空間整合。在 libbpf 中,使用者空間程式負責:
- 載入 eBPF 物件檔案
- 設定對映初始值
- 附加 eBPF 程式到適當的事件
- 從對映或效能緩衝區讀取資料
以下是 hello-buffer-config.c
中使用者空間程式碼的簡化版本:
int main(int argc, char **argv)
{
struct hello_buffer_config_bpf *obj;
int err;
// 設定 libbpf 錯誤和除錯回呼
libbpf_set_print(libbpf_print_fn);
// 開啟和載入 eBPF 物件
obj = hello_buffer_config_bpf__open_and_load();
if (!obj) {
fprintf(stderr, "Failed to open and load BPF object\n");
return 1;
}
// 設定效能緩衝區回呼
struct perf_buffer *pb = NULL;
struct perf_buffer_opts pb_opts = {};
pb_opts.sample_cb = handle_event;
pb = perf_buffer__new(bpf_map__fd(obj->maps.output), 8, &pb_opts);
// 附加 eBPF 程式
err = hello_buffer_config_bpf__attach(obj);
if (err) {
fprintf(stderr, "Failed to attach BPF program\n");
goto cleanup;
}
// 主事件迴圈
while (1) {
perf_buffer__poll(pb, 100);
}
cleanup:
hello_buffer_config_bpf__destroy(obj);
return err != 0;
}
這段使用者空間程式碼負責載入和管理 eBPF 程式。它執行以下關鍵步驟:
- 設定 libbpf 的錯誤和除錯處理函式。
- 開啟並載入 eBPF 物件檔案,這包含了我們的 eBPF 程式和對映定義。
- 設定效能緩衝區回呼函式,用於處理從 eBPF 程式傳送的事件。
- 附加 eBPF 程式到適當的事件(在這裡是 openat 系統呼叫)。
- 進入主事件迴圈,持續從效能緩衝區讀取事件。
- 最後在清理時釋放資源。
libbpf 自動生成了許多輔助函式,如 hello_buffer_config_bpf__open_and_load()
和 hello_buffer_config_bpf__attach()
,這些函式簡化了 eBPF 程式的載入和附加過程。
CO-RE 的未來發展
CO-RE 技術代表了 eBPF 生態系統的重要進步,使 eBPF 程式能夠在不同的核心版本上執行,而無需重新編譯或修改。隨著 libbpf 的成熟和更廣泛的採用,玄貓預見未來幾年 CO-RE 將成為 eBPF 程式開發的標準方法。
特別是,以下幾個領域值得關注:
工具鏈改進 - 更好的編譯器整合和除錯工具將簡化 CO-RE eBPF 程式的開發。
語言支援擴充套件 - 除了 C 和 Rust 外,可能會有更多語言獲得 CO-RE eBPF 支援。
更高階的抽象 - 建立在 libbpf 之上的框架將提供更高階的抽象,進一步簡化 eBPF 程式開發。
跨發行版標準化 - 隨著 libbpf 成為標準套件,跨發行版的 eBPF 程式佈署將變得更加無縫。
eBPF 和 CO-RE 技術的結合為 Linux 系統可觀測性、網路和安全領域開啟了新的可能性,讓開發者能夠建立強大與可攜的核心擴充套件。
結語
CO-RE 技術為 eBPF 程式開發帶來了革命性的變化,使開發者能夠編寫一次程式碼,然後在各種核心版本上執行。透過 BTF 和 libbpf 的結合,CO-RE 解決了 eBPF 程式可攜性的長期挑戰。
在本文中,玄貓探討了 CO-RE eBPF 程式的基本結構,包括標頭檔案的組織、對映定義、程式段落和使用者空間整合。雖然相較於 BCC 框架,使用 libbpf 和 CO-RE 需要更多的樣板程式碼,但它提供了更好的效能、更少的依賴和更好的可攜性。
隨著 eBPF 生態系統的不斷發展,CO-RE 將繼續發揮關鍵作用,讓開發者能夠建立強大、高效與可攜的核心擴充套件,無需擔心核心版本的差異。無論是系統可觀測性、網路還是安全領域,CO-RE eBPF 程式都將成為關鍵工具。