經典 bpf 範例: bootstrap 分析 - eBPF基礎知識 Part3

MarkZhu發表於2023-03-26

經典 libbpf 範例: bootstrap 分析 - eBPF基礎知識 Part3

《eBPF基礎知識》 系列簡介:

《eBPF基礎知識》系列目標是整理一下 BPF 相關的基礎知識。主要聚焦程式與核心互動介面部分。文章使用了 libbpf,但如果你不直接使用 libbpf,看本系列還是有一定意義的,因為它聚焦於程式與核心互動介面部分,而非 libbpf 封裝本身。而所有 bpf 開發框架,都要以相似的方式跟核心互動。甚至框架本身就是基於 libbpf。哪怕是 golang/rust/python/BCC/bpftrace。

  1. 《ELF 格式簡述 - eBPF基礎知識 Part1》
  2. 《BPF 系統介面 與 libbpf 示例分析 - eBPF基礎知識 Part2》

上期 《BPF 系統介面 與 libbpf 示例分析 - eBPF基礎知識 Part2》 介紹了一個最簡的 BPF 程式如何與核心互動。

這期,將圖解分析一個更為現實的實用的 BPF 程式與核心的互動過程。國際習慣:儘量多圖少文字。以下假設讀者已經對 BPF 有一定的瞭解,或者閱讀過之前的 《eBPF基礎知識》系列文章。

libbpf 提供了一個使用 libbpf 的示例:https://github.com/libbpf/libbpf-bootstrap。其中的 bootstrap 程式示範了一個最簡單但現實實用的 BPF 程式載入、執行、與核心互動的過程。下面將圖解分析這個程式與核心的互動過程。

動機:為何我想學習 BPF

開始分析前,我想說幾句廢話:為何我想學習 BPF?

因為是熱點啊 :) 。看,當年的 Block-Chain、AI、CloudNative。如今的 ChatGPT。我承認,35 歲前的我真會這樣考慮問題。而且,如果讓我帶著現在的認知回到 35 歲前的身體上,說不定心底也會是這個答案。

但現在,我更想運用 BPF 來有個更深遠的底層技術觀察力:

  1. 加速核心知識學習

    Linux 核心現在已經是個很複雜的怪獸。很難簡單直接閱讀原始碼去理解其中的設計思想了。但只看書的話,你有會一種“書上得來終覺淺”的感覺。BPF trace 核心,keep your hands dirty。是比較好的中間方法。可以保持學習的興趣,讓學習不失實用性。

  2. 核心可觀察性

    這個不用多說,BPF 的強項。Cloud Native + 網路監控 + 安全 將會是 BPF 的殺手應用。

  3. 應用可觀察性

bootstrap 程式功能

bootstrap 程式本身類似經典的 execsnoop。監聽核心的 exec 呼叫,輸出到程式終端:

$ sudo ./bootstrap -d 50
TIME     EVENT COMM             PID     PPID    FILENAME/EXIT CODE
19:18:32 EXIT  timeout          3817109 402466  [0] (126ms)
19:18:32 EXIT  sudo             3817117 3817111 [0] (259ms)
19:18:32 EXIT  timeout          3817110 402466  [0] (264ms)
19:18:33 EXIT  python3.7        3817083 1       [0] (1026ms)
19:18:38 EXIT  python3          3817429 3817424 [1] (60ms)
19:18:38 EXIT  sh               3817424 3817420 [0] (79ms)
19:18:38 EXIT  timeout          3817420 402466  [0] (80ms)
...

程式架構

image.png

bootstrap 與核心互動概述

如上圖排版有問題,請點這裡用 Draw.io 開啟。部分帶互動連結和 hover tips

上圖 file descriptor 之間的連線,反映了它們之間的關聯。這裡簡單列一下上圖的流程:

  1. 其它程式呼叫的 exec ,觸發 sched_process_exec tracepoint
  2. 核心呼叫相關的 BPF 程式,更新 exec_start MAP
  3. BPF 程式把事件提交到 ringbuffer MAP
  4. 應用(bootstrap(user space)) 讀取 ringbuffer

核心態 BPF

在 make 的過程中,實際上是執行了:

clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I.output -I../../libbpf/include/uapi -I../../vmlinux/x86/ 
-idirafter /usr/lib/llvm-14/lib/clang/14.0.0/include -idirafter /usr/local/include -idirafter 
/usr/include/x86_64-linux-gnu -idirafter /usr/include -c minimal.bpf.c -o .output/bootstrap.bpf.o

llvm-strip -g .output/bootstrap.bpf.o

最後一行就是重點。輸入是 bootstrap.bpf.c。輸出是 bootstrap.bpf.o。這是一個 ELF 格式的檔案。這個檔案將會嵌入到應用中。bootstrap.bpf.o section 如下:

$ readelf -aW examples/c/.output/bootstrap.bpf.o

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .strtab           STRTAB          0000000000000000 008936 0000f2 00      0   0  1
  [ 2] .text             PROGBITS        0000000000000000 000040 000000 00  AX  0   0  4
  [ 3] tp/sched/sched_process_exec PROGBITS        0000000000000000 000040 0001f8 00  AX  0   0  8
  [ 4] .reltp/sched/sched_process_exec REL             0000000000000000 008370 000030 10   I 15   3  8
  [ 5] tp/sched/sched_process_exit PROGBITS        0000000000000000 000238 0002a8 00  AX  0   0  8
  [ 6] .reltp/sched/sched_process_exit REL             0000000000000000 0083a0 000050 10   I 15   5  8
  [ 7] license           PROGBITS        0000000000000000 0004e0 00000d 00  WA  0   0  1
  [ 8] .rodata           PROGBITS        0000000000000000 0004f0 000008 00   A  0   0  8
  [ 9] .maps             PROGBITS        0000000000000000 0004f8 000030 00  WA  0   0  8
  [10] .BTF              PROGBITS        0000000000000000 000528 0077a9 00      0   0  4
  [11] .rel.BTF          REL             0000000000000000 0083f0 000040 10   I 15  10  8
  [12] .BTF.ext          PROGBITS        0000000000000000 007cd4 00054c 00      0   0  4
  [13] .rel.BTF.ext      REL             0000000000000000 008430 000500 10   I 15  12  8
  [14] .llvm_addrsig     LOOS+0xfff4c03  0000000000000000 008930 000006 00   E  0   0  1
  [15] .symtab           SYMTAB          0000000000000000 008220 000150 18      1   8  8

如果你不太瞭解 ELF 格式,建議先看看,因為理解這個格式很重要。可以參考我的《ELF 格式簡述 - eBPF 基礎知識》

make 使用者態應用

這裡主要講 skeleton 部分了。以前做過舊 RPC 的同學可能比較瞭解。用一些資料去生成一個 skeleton(骨架)程式碼 (主要是一些資料結構和函式定義),方便使用者基於這些 skeleton 再開發程式。對於 libbpf,也是一樣的。

$ bpftool gen skeleton .output/bootstrap.bpf.o
Successfully remade target file '.output/bootstrap.skel.h'

bpftool 分析 BPF核心態的 ELF 檔案(bootstrap.bpf.o),生成 skeleton 程式碼 。應用就可以基於這個 skeleton 去開發了。

需要注意的是,生成的 bootstrap.skel.h 其實嵌入了 bootstrap.bpf.o

static inline const void *bootstrap_bpf__elf_bytes(size_t *sz)
{
    *sz = 2432;
    return (const void *)"\
\x7f\x45\x4c\x46\x02\x01\x01\0\0\0\0\0\0\0\0\0\x01\0\xf7\0\x01\0\0\0\0\0\0\0\0\
\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\0\0\0\0\0\0\0\x40\0\0\0\0\0\x40\0\x0e\0\x01\
...

跟蹤 make 過程的小技巧

由於 c/c++ 我已經放下了快 20 年了。對 make 過程的 debug 已經忘記了。還好,有搜尋引擎。

要知道 make 過程實際上發生了什麼,執行了什麼 clang/gcc 命令。你當然可以看 Makefile 。但如果我一直相信 trace > source code review 。如何 trace ? 我用了個老土的方法:

make clean && reset && make SHELL="/bin/bash -x" --debug=bvi 2>&1 | tee -a make.log

其中關鍵是 SHELL="/bin/bash -x" 了。

分析程式載入、執行、核心互動過程

終於回到我的初心了。

image.png

bootstrap 與核心互動過程

如上圖排版有問題,請點這裡用 Draw.io 開啟。部分帶互動連結和 hover tips

圖中是我跟蹤的結果。用 Draw.io 開啟後,每一步均有 link,點選可看到程式碼。滑鼠放到連線線上,會 hover 出 stack(呼叫堆疊)。

圖中的說明已經比較詳細。其中包括重要的資料結構和步驟。

我 fork 了專案到這裡:

https://github.com/labilezhu/libbpf-bootstrap/tree/20230226

vscode debug 配置

你可以看到我用 vscode debug,其中 .vscode/launch.json 配置如下:

{
    "configurations": [
        //bootstrap
        {
            "name": "gdb bootstrap",
            
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/examples/c/bootstrap",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                },
                {
                    "description": "Set Disassembly Flavor to Intel",
                    "text": "-gdb-set disassembly-flavor intel",
                    "ignoreFailures": true
                }
            ],
            // "preLaunchTask": "C/C++: gcc build active file",
            "miDebuggerPath": "/usr/bin/sudo-gdb"
        },   
    "version": "2.0.0"
}

因為 bpf 程式需要 root 許可權,所以要加上 "miDebuggerPath": "/usr/bin/sudo-gdb"。而 /usr/bin/sudo-gdb 內容如下:

$ cat /usr/bin/sudo-gdb
sudo /usr/bin/gdb "$@"

gdb 斷點設定

由於目的是觀察 bpf 載入過程相關的 syscall。可以配置 gdb 的 syscall 斷點:

-exec catch syscall

其中 -exec 字首是 vscode 對直接使用 gdb 命令要求加的字首。

gdb 斷點設定的一些坑

libbpf 自身時常會檢查執行期核心的對 bpf 特性的支援情況。所以有一些 syscall 是要手工忽略的。如 stack 中有 kernel_supports(...) 的均是可以忽略的。有沒方法讓 gdb 加個斷點條件?當然有:

-exec catch syscall
Catchpoint 3 (any syscall) //這裡注意,3 是斷點的 id ,下面的命令要引用這個 id。在您的環境可能數值不同。
-exec condition 3 !$_any_caller_matches("get_kernel_version|kernel_supports|bpf_object__probe_loading|btf_parse_raw|handle_event", 20)

即,stack 中已經包含 kernel_supports 等等的,就不斷點。

後記

這個後記和本文沒什麼相關了,不喜可跳過。只是最近心情一般,春暖花開,本應該很快樂的。畢竟 blog 的本質是記錄,所以也上圖一張,記錄一下心情。

image.png