BPF 進階筆記(四):除錯 BPF 程式

語言: CN / TW / HK

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, ...)

static __always_inline u64 ____##name(__BPF_MAP(x, __BPF_DECL_ARGS,

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)
「其他文章」