BPF 進階筆記(四):除錯 BPF 程式
BPF 進階筆記(四):除錯 BPF 程式 Published at 2022-05-02 | Last Update 2022-05-02
本文是閱讀一些 BPF 高階教程時所作的筆記。
關於 “BPF 進階筆記” 系列
平時學習和使用 BPF 時所整理。由於是筆記而非教程,因此內容不會追求連貫,有基礎的 同學可作查漏補缺之用。
文中涉及的程式碼,如無特殊說明,均基於核心 5.10 版本。
BPF 進階筆記(一):BPF 程式(BPF Prog)型別詳解:使用場景、函式簽名、執行位置及程式示例 BPF 進階筆記(二):BPF Map 型別詳解:使用場景、程式示例 BPF 進階筆記(三):BPF Map 核心實現 BPF 進階筆記(四):除錯 BPF 程式
關於 “BPF 進階筆記” 系列 1 列印日誌
1.1 日誌路徑及格式 1.2 bpf_printk():kernel 5.2+
使用方式 使用限制 核心實現
1.3 bpf_trace_printk() 使用方式 使用限制 核心實現
2 用 BPF 程式 trace 另一個 BPF 程式(BPF trampoline)
2.1 使用場景 2.2 依賴:kernel 5.5+
3 設定斷點,單步除錯
3.1 bpf_dbg(僅限 cBPF)
1 列印日誌
1.1 日誌路徑及格式
本節將介紹的幾種列印日誌方式最終都會輸出到 debugfs 路徑 /sys/kernel/debug/tracing/trace:
$ sudo tail /sys/kernel/debug/tracing/trace
欄位說明
-
telnet-470 [001] .N.. 419421.045894: 0x00000001: <formatted msg>
以上看到的是預設 trace 輸出格式,
可通過 /sys/kernel/debug/tracing/trace_options 定製化 trace 輸出格式(列印哪些列); 另外還可參考 /sys/kernel/debug/tracing/README,其中有更詳細的說明。
欄位說明:
telnet:程序名; 470:程序 ID; 001:程序所在的 CPU;
.N..:每個字元表示一組配置選項,依次為, 是否啟用了中斷(irqs); 排程選項,這裡 N 表示設定了 TIF_NEED_RESCHED 和 PREEMPT_NEED_RESCHED 標誌位; 硬中斷/軟中斷是否正在執行; level of preempt_disabled
419421.045894:時間戳;
0x00000001:BPF 使用的一個 fake value,for instruction pointer register;
1.2 bpf_printk():kernel 5.2+
使用方式
這是核心 libbpf 庫提供的一個巨集:
// http://github.com/torvalds/linux/blob/v5.10/tools/lib/bpf/bpf_helpers.h#L17
/* Helper macro to print out debug messages */ #define bpf_printk(fmt, ...)
({
char ____fmt[] = fmt;
bpf_trace_printk(____fmt, sizeof(____fmt),
## VA_ARGS );
})
使用非常方便,和 C 的 printf() 差不多,例如,
bpf_printk("tcp_v4_connect latency_us: %u", latency_us);
使用限制
需要核心 5.2+,否則編譯能通過,但執行時會報錯: map .rodata: map create: read- and write-only maps not supported (requires >= v5.2) 這個錯誤提示非常奇怪(實際上目前來說,大部分 BPF 錯誤提示都不那麼直接)。 簡單來說,BPF 的棧空間非常小,每次呼叫 bpf_printk() 都會動態宣告一個 char ____fmt[] = fmt; 並放到棧上,導致效能很差。
5.2 引入了 BPF global (and static) 變數,因此 clang 在編譯時 可以直接將這些變數放到 ELF 的只讀區域(.rodata,read-only data),libbpf 載入程式時將這些資料放到一個 .rodata BPF map 中,程式在用到這些變數時,背後執行一次 map lookup 即可。 相比於每次都在棧上建立一個字元陣列(字串),這樣更加快速和高效。
更多內容,見 Andrii Nakryiko 的部落格 Improving bpf_printk() 。 最多隻能帶 3 個引數,即 bpf_printk(fmt, arg1, arg2, arg3)。 這是由 bpf_trace_printk() 的限制決定的,下一節有具體解釋。
核心實現
前面已經看到 bpf_printk() 非常簡單,只是單純封裝了一下 bpf_trace_printk(), 後者定義在 include/uapi/linux/bpf.h,具體實現見下文。
1.3 bpf_trace_printk()
對於 5.2 以下的核心,列印日誌可以用 bpf_trace_printk(),它比 bpf_printk() 要麻煩一點:要提前宣告格式字串 fmt。
使用方式
// http://github.com/torvalds/linux/blob/v5.10/include/uapi/linux/bpf.h#L772
/**
-
long bpf_trace_printk(const char *fmt, u32 fmt_size, ...) */
功能與 printk() 類似,按指定格式將日誌列印到 /sys/kernel/debug/tracing/trace 中; 但支援的格式比 printk() 少;
5.10 支援 %d, %i, %u, %x, %ld, %li, %lu, %lx, %lld, %lli, %llu, %llx, %p, %s。不支援指定字串或數字長度等,否則會返回 -EINVAL(同時什麼都不列印)。 5.13 有進一步增強,見 Detecting full-powered bpf_trace_printk()。
每次呼叫這個函式時,會往 trace 中追加一行;當 /sys/kernel/debug/tracing/trace is open,日誌會被丟棄, 可使用 /sys/kernel/debug/tracing/trace_pipe 來避免這種情況; 這個函式執行很慢,因此只應在除錯時使用;
fmt 格式串是否有預設換行: 5.9 之前沒有,需要自己加 \n; 5.9+ 會預設加一個換行符,patch 見 bpf: Use dedicated bpf_trace_printk event instead of trace_printk()。
函式的返回值是寫到 buffer 的位元組數,出錯時返回負的 error code。
例子:
char fmt[] = "tcp_v4_connect latency_us: %u"; bpf_printk(fmt, sizeof(fmt), latency_us);
使用限制
最多隻能帶 3 個引數(這是因為 eBPF helpers 最多隻能帶 5 個引數,前面 fmt 和 fmt_size 已經佔了兩個了); 使用該函式的程式碼必須是 GPL 相容的; 前面已經提到,格式字串支援的型別有限,但 5.13 有進一步改進,詳見 Detecting full-powered bpf_trace_printk()。
核心實現
實現:
// http://github.com/torvalds/linux/blob/v5.10/kernel/trace/bpf_trace.c#L428
BPF_CALL_5(bpf_trace_printk, char *, fmt, u32, fmt_size, u64, arg1, u64, arg2, u64, arg3) { ... }
其中 BPF_CALL_5 的定義:
// http://github.com/torvalds/linux/blob/v5.10/include/linux/filter.h#L485
#define BPF_CALL_x(x, name, ...)
BPF_V, VA_ARGS ));
##name)(__BPF_MAP(x, __BPF_DECL_ARGS, __BPF_V, VA_ARGS ));u64 name(__BPF_REG(x, __BPF_DECL_REGS, __BPF_N, VA_ARGS ));
BPF_DECL_REGS,BPF_N, VA_ARGS )) {
##name) ##name)(__BPF_MAP(x,__BPF_CAST,__BPF_N, VA_ARGS ));}
static __always_inline u64 ____##name(__BPF_MAP(x, __BPF_DECL_ARGS, __BPF_V, VA_ARGS ))
#define BPF_CALL_5(name, ...) BPF_CALL_x(5, name, VA_ARGS )
2 用 BPF 程式 trace 另一個 BPF 程式(BPF trampoline)
2.1 使用場景
BPF trampoline 可以 作為核心函式之間、BPF 程式和其他 BPF 程式之間的橋樑。使用場景之一就是 tracing 其他 BPF 程式。這個功能來自 XDP 開發過程中的痛點。 現在能向任何網路型別的 BPF 程式 attach 類似 fentry/fexit 的 BPF 程式,因 此能夠看到 XDP、TC、LWT、cgroup 等任何型別 BPF 程式中包的進進出出,而不會影 響到這些程式的執行,大大降低了基於 BPF 的網路排障難度。
一些 patch,如果感興趣:
libbpf bpf btf/verifier libbpf add fentry/fexit attach type trampoline impl: jit_com, trampoline, verifier, btf, good doc
BPF trampoline 其他使用場景:
fentry/fexit BPF 程式:功能與 kprobe/kretprobe 類似,但效能更好,幾乎沒有效能開銷(practically zero overhead);
動態連結 BPF 程式(dynamicly link BPF programs)。 在 tracing、networking、cgroup BPF 程式中,中,是比 prog array 和 prog link list 更加通用的機制。
在很多情況下,可直接作為基於 bpf_tail_call 程式鏈的一種替代方案。
這些特性都需要 root 許可權。
2.2 依賴:kernel 5.5+
3 設定斷點,單步除錯
3.1 bpf_dbg(僅限 cBPF)
見 (譯) Linux Socket Filtering (LSF, aka BPF)(Kernel,2021)。
« [譯] BPF ring buffer:使用場景、核心設計及程式示例(2020)
- Linux 網路棧接收資料(RX):原理及核心實現
- K8s 的核心是 API 而非容器:從理論到 CRD 實踐(2022)
- BPF 進階筆記(四):除錯 BPF 程式
- [譯] BPF ring buffer:使用場景、核心設計及程式示例(2020)
- [譯] [論文] BBR:基於擁塞(而非丟包)的擁塞控制(ACM, 2016)
- [譯] 為 K8s workload 引入的一些 BPF datapath 擴充套件(LPC, 2021)
- [譯] [論文] 可虛擬化第三代(計算機)架構的規範化條件(ACM, 1974)
- [譯] NAT 穿透是如何工作的:技術原理及企業級實踐(Tailscale, 2020)
- [譯] 寫給工程師:關於證書(certificate)和公鑰基礎設施(PKI)的一切(SmallStep, 2018)
- [譯] 基於角色的訪問控制(RBAC):演進歷史、設計理念及簡潔實現(Tailscale, 2021)
- [譯] Control Group v2(cgroupv2 權威指南)(KernelDoc, 2021)
- [譯] Linux Socket Filtering (LSF, aka BPF)(KernelDoc,2021)
- [譯] LLVM eBPF 彙編程式設計(2020)
- [譯] Cilium:BPF 和 XDP 參考指南(2021)
- BPF 進階筆記(三):BPF Map 核心實現
- BPF 進階筆記(二):BPF Map 型別詳解:使用場景、程式示例
- BPF 進階筆記(一):BPF 程式型別詳解:使用場景、函式簽名、執行位置及程式示例
- 原始碼解析:K8s 建立 pod 時,背後發生了什麼(四)(2021)
- 原始碼解析:K8s 建立 pod 時,背後發生了什麼(三)(2021)
- [譯] 邁向完全可程式設計 tc 分類器(NetdevConf,2016)