# 虛擬化-lab1 ## eBPF 介紹 屬於核心(kernel)程式開發。與核心模組主要差別在**驗證器**(verifier)和 **map** 的設計。驗證器由 Linux kernel 提供,確認被加載的程式的正確性。map 則提供便利的通訊介面,讓 BPF 核心程式間和用戶程式能夠輕易的協作。 ### 追蹤程式介紹 eBPF 程式的兩個目的:觀測和改變核心行為,用於觀測的程式可以歸類為追蹤(trace)的類別。 追蹤程式底下可以再分成 kprobe/kretprobe, fentry/fexit 和 tracepoint : - kprobe/kretprobe, fentry/fexit: 主要觀察核心中的函式的進入和退出,可以取得呼叫函式的引數(arguments)和回傳值。被追蹤的程式無須修改和重新編譯,屬於動態追蹤的技術。 - tracepoint: 核心程式碼設立和提供 tracepoint 以供觀測,屬於靜態追蹤。這套機制由 Linux 提供,屬於穩定的公開介面。 ### 系統架構 系統元件圖 ![System Block Diagram](https://hackmd.io/_uploads/ByPo075gJe.png) - `hellp.py`、`hello()`:eBPF 程式,有核心和用戶兩個部分。課程中兩個部分都用 C 開發 - `trace pseudofile`:eBPF 程式核心和用戶溝通的機制,透過特殊檔案或 map - `execve`、`小蜜蜂`:核心中的 tracepoint ,觸發附著的 eBPF 核心程式 - `Apps`:系統上所有應用程式,可能在系統呼叫時進入 tracepoint ## lab1 目標 1. 編譯 eBPF 程式:撰寫 makefile ,編譯提供的程式碼成執行檔並且執行 - minimal:附著(attach)程式到系統呼叫 `write()` ,輸出除錯資訊。 - boostrap:附著到程式執行和結束的 tracepoint ,測量執行時間。 1. 撰寫追蹤程式:開發兩種 eBPF 追蹤程式,計算 TCP rtt。 ## 目標一:編譯 eBPF 程式 ### 開發工具和設定 - 此篇省略系統設定,例如 Linux Kconfig 的設定 - 開發工具包含函式庫 libbpf (以及相依的 zlib、libelf)和命令列工具 bpftool。 ### 檔案介紹 - `<app>.bpf.c` :核心程式,會轉成 `<app>.skel.h` skeleton 檔供用戶態程式使用 - `<app>.c` :用戶程式,加載和附著核心程式 - `<app>.h` :定義核心和用戶溝通時共用的類別 - `vmlinux.h` :具有 kernel 內所有類別的定義 - `bpftool` :生成 skeleton 和 `vmlinux.h` ### minimal 簡介 核心監測到用戶使用系統呼叫 `write()` 時輸出訊息,用戶附著完核心程式後會不斷使用 `write()` ### minimal 核心程式 ```clike // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause /* Copyright (c) 2020 Facebook */ // 這裡不符常規,用 "linux/bpf.h" 取代 "vmlinux.h" // 需要定義在 bpf header 前面,因為 header 需要 kernel 類別 #include <linux/bpf.h> #include <bpf/bpf_helpers.h> // 部分 bpf api 需要 license // SEC("license") 指定 symbol's section char LICENSE[] SEC("license") = "Dual BSD/GPL"; // supported since Linux 5.5 int my_pid = 0; // define program type and attachment point SEC("tp/syscalls/sys_enter_write") int handle_tp(void *ctx) { // return u64. [63: 32] is tgid, [31: 0] is pid // tgid is pid and pid is tid in kernel terminology int pid = bpf_get_current_pid_tgid() >> 32; if (pid != my_pid) return 0; // number of arguments is limited bpf_printk("BPF triggered from PID %d.\n", pid); return 0; } ``` 依據程式類型,函式有對應的簽名。 `handle_tp` 屬於 tracepoint ,參數只有一個指標,類別則是 tracepoint 名稱加上前綴 `trace_event_raw_` ,定義在 `vmlinux.h` ### minimal 用戶程式 #### minimal.skel.h ```clike ... // kernel 程式對應的結構體 // 在呼叫 api 會使用 maps/progs/links // bss/data/rodata 用戶可以直接存取 struct minimal_bpf { struct bpf_object_skeleton *skeleton; struct bpf_object *obj; struct { struct bpf_map *bss; } maps; struct { struct bpf_program *handle_tp; } progs; struct { struct bpf_link *handle_tp; } links; struct minimal_bpf__bss { int my_pid; } *bss; }; ``` skeleton 提供用戶的 api ,簡單的 eBPF 用戶程式可以不用自己呼叫 libbpf ,只需要使用 skeleton api ```clike // setup static inline struct minimal_bpf *minimal_bpf__open(void) { ... } static inline int minimal_bpf__load(struct minimal_bpf *obj) { ... } static inline struct minimal_bpf *minimal_bpf__open_and_load(void) { ... } static inline int minimal_bpf__attach(struct minimal_bpf *obj) { ... } // teardown static inline void minimal_bpf__destroy(struct minimal_bpf *obj) { ... } static inline void minimal_bpf__detach(struct minimal_bpf *obj) { ... } ``` #### minimal.c ```clike int main(int argc, char **argv) { struct minimal_bpf *skel = minimal_bpf__open(); skel->bss->my_pid = getpid(); // set global variable err = minimal_bpf__load(skel); // Load & verify BPF programs err = minimal_bpf__attach(skel); for (;;) { fprintf(stderr, "."); sleep(1); } minimal_bpf__destroy(skel); ... } ``` - 全域變數必須在程式加載前設定 - tracepoints, kprobes 和特定類型的 eBPF 程式 libbpf 可以查看 `SEC()` 進行自動附著 ### eBPF 編譯流程 ![截圖 2024-11-08 下午3.50.40](https://hackmd.io/_uploads/SkVxXBs-yg.png) ### makefile #### 基本規則 makefile 是自動化建置工具,透過撰寫文字檔定義檔案的相依和生成規則,將程式碼編譯成執行檔。 ```python # 檔名為 makefile 或 Makefile 目標文件 ...:依賴項目 ... # 多個項目透過空格分開 終端命令 # 開頭一定要是 TAB ... ``` 在 makefile 檔案所在的目錄執行 `make <目標文件>` 生成特定目標, `make` 則是生成第一個規則的目標。 特殊符號說明: - `$^`: 所有依賴項目 - `$<`: 第一個依賴項目 - `$@`: 目標文件 #### 示範 makefile ```python # generate vmlinux header vmlinux.h: bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h # build bpf kernel object file minimal.bpf.o: minimal.bpf.c vmlinux.h clang -g -O2 -target bpf -c $< -o $@ # generate skeleton minimal.skel.h: minimal.bpf.o bpftool gen skeleton $< > $@ # build app minimal: minimal.c minimal.skel.h clang -g minimal.c -lbpf -lelf -lz -o $@ ``` - `-O2`, `-target bpf` 是必要的選項 - `-lbpf -lelf -lz` 執行檔動態連結函式庫 - 執行檔不用連結核心目標檔 ### 除錯相關 - 需要提升權限執行程式,也就是使用 `sudo ./<executable>` 執行 - 查看 eBPF 輸出訊息 ```shell $ sudo cat /sys/kernel/debug/tracing/trace_pipe <...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: BPF triggered from PID 3840345. <...>-3840345 [010] d... 3220702.101265: bpf_trace_printk: BPF triggered from PID 3840345. ``` - 查看當前附著的程式 ```shell $ sudo bpftool perf show pid 232272 fd 17: prog_id 394 kprobe func do_execve offset 0 pid 232272 fd 19: prog_id 396 tracepoint sys_enter_execve ``` ### boostrap 簡介 - 程式執行的時候,在 hash map 紀錄 pid 和時間 - 程式結束的時候,利用 pid 查詢執行開始的時間,計算時長。將資訊透過 ring buffer 傳遞給用戶 ### hash map eBPF map 的定義程式碼如下,根據種類稍有差異, hash map 包含 `type`、`key`、`value` ```clike struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 8192); __type(key, pid_t); __type(value, u64); } exec_start SEC(".maps"); ``` 核心態的 map 操作 api :::spoiler ```clike /* * bpf_map_lookup_elem * * Perform a lookup in *map* for an entry associated to *key*. * * Returns * Map value associated to *key*, or **NULL** if no entry was * found. */ static void *(* const bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1; /* * bpf_map_update_elem * * Add or update the value of the entry associated to *key* in * *map* with *value*. *flags* is one of: * * **BPF_NOEXIST** * The entry for *key* must not exist in the map. * **BPF_EXIST** * The entry for *key* must already exist in the map. * **BPF_ANY** * No condition on the existence of the entry for *key*. * * Flag value **BPF_NOEXIST** cannot be used for maps of types * **BPF_MAP_TYPE_ARRAY** or **BPF_MAP_TYPE_PERCPU_ARRAY** (all * elements always exist), the helper would return an error. * * Returns * 0 on success, or a negative error in case of failure. */ static long (* const bpf_map_update_elem)(void *map, const void *key, const void *value, __u64 flags) = (void *) 2; /* * bpf_map_delete_elem * * Delete entry with *key* from *map*. * * Returns * 0 on success, or a negative error in case of failure. */ static long (* const bpf_map_delete_elem)(void *map, const void *key) = (void *) 3; ``` ::: :::info 核心及用戶 api 格式可能很像,但兩者是不同概念,一個使用系統呼叫,一個使用核心內的函式。 ::: ### ring buffer 從核心傳遞資料到用戶程式,api 操作流程大致為: ```clike // 分配記憶體 struct event *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); // 設定 e 的數值... // 發送資料 bpf_ringbuf_submit(e, 0); ``` 用戶接收部分則是: ```clike // * 獲取資源,用 skeleton maps 下的成員指定 map // 在程式加載之後才能操作 map // * callback "handle_event" 來處理資料 rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL); ... // 處理資料的迴圈 while (!exiting) { err = ring_buffer__poll(rb, 100 /* timeout, ms */); } ... // 釋放資源 ring_buffer__free(rb); ``` ### boostrap 核心程式 - 讀取系統核心的資料或記憶體,需要特別的函式。例如從系統取得目前行程的 `struct task_struct` 讀取 ppid (parent pid) ```clike struct task_struct *task = (struct task_struct *)bpf_get_current_task(); pid_t ppid = BPF_CORE_READ(task, real_parent, tgid); ``` 功能在普通 c 語言等同於 ```clike pid_t ppid = task->real_parent->tgid; ``` 已經宣告變數時,要傳入指標和大小,同系列的 api 有: ```clike BPF_CORE_READ_INTO(&ppid, task, real_parent, tgid); bpf_core_read(&ppid, sizeof(ppid), real_parent); ``` - 從參數中讀取資料 ```clike SEC("tp/sched/sched_process_exec") int handle_exec(struct trace_event_raw_sched_process_exec *ctx) { ... fname_off = ctx->__data_loc_filename & 0xFFFF bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off); } ``` - 唯讀的全域變數定義須包含 const volatile ,跟 MMIO 操作的道理相同 ```clike const volatile unsigned long long min_duration_ns = 0; ``` ### 目標一:練習項目 - 撰寫 makefile ,使用命令 `make <target>` 編譯 minimal 和 boostrap 執行檔 - 觀察 minimal 和 boostrap 輸出,確認程式正常運作 - 閱讀 boostrap 程式碼以下功能的實作,目標二要實作同樣功能: - 使用 hash map 紀錄時間 - 用戶和核心使用 ring buffer 傳遞資料 ## 目標二:撰寫追蹤程式 開發 fentry、tracepoint 兩種追蹤程式,輸出 TCP/IPV4 的延遲時間(round trip time)。範例輸出如下 ```shell $ sudo ./tcprtt_tp PID COMM SRC DST LAT(ms) 0 swapper/0 192.168.68.64 :53393 --> 20.42.65.94 :47873 196934.87 0 swapper/0 192.168.68.64 :52931 --> 172.217.163.36 :20480 6854.23 ``` fentry 使用 ring buffer , tracepoint 使用 ring buffer 和 hash map,可以參考 bootstrap 使用 map 的流程 ### fentry、tracepoint 實作共通說明 - 流程: - 核心程式:搜集網路通訊資料,使用 ring buffer 傳遞到用戶 - 用戶程式:加載和附著核心程式,迴圈不斷接收 ring buffer 資料輸出到終端 - 提供的檔案介紹: - **tcprtt.c**:fentry 用戶的完整程式 - **tcprtt_tp.c**:tracepoint 用戶的完整程式 - **tcprtt.bpf.c**:待完成的 fentry 核心程式 - **tcprtt_tp.bpf.c**:待完成的 tracepoint 核心程式 ### fentry 實作說明 `void tcp_rcv_established(struct sock *sk, struct sk_buff *skb);` 上面是附著的核心函式。當連線建立以後,每次用戶傳輸都會呼叫此函式。 fentry 的函式簽名會與附著的函式相同,並且可以讀取引數,例如 `sk`、`skb`,就可以藉此得到所需資訊。 ### tcprtt.bpf.c 示範程式碼 ```clike #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #include <bpf/bpf_core_read.h> #include <bpf/bpf_endian.h> // for the definition of the type shared between user and kernel #include "tcprtt.h" #include "bpf_tracing_net.h" // TODO: define ring buffer SEC("fentry/tcp_rcv_established") int BPF_PROG(tcp_rcv, struct sock *sk /*, optional */) { // handler ipv4 only if (sk->__sk_common.skc_family != AF_INET) return 0; // TODO: // 蒐集 ip, port... // ring buffer 發送蒐集的資料 return 0; } ``` 使用 `BPF_PROG()` 定義 fentry 函式, `tcp_rcv` 是實際函式的名稱,後面則是參數,要依序對應附著的核心函式的參數。 ### 蒐集資料 - pid, command:使用 `bpf_get_current_pid_tgid()`、`bpf_get_current_comm()`,用法參考 minimal 和 bootstrap - ip, port 和 rtt:需要從 `struct sock *sk` 取得 #### ip 和 port 被封裝在 `__sk_common` 中 ```clike struct sock { struct sock_common __sk_common; ... }; ``` 類別其實都是整數,然後 `skc_rcv_saddr` 是來源 ip ,`skc_num` 是來源 port 。 只有 `skc_num` 是以 host endian ,其他皆為 network endian ```clike /** * struct sock_common - minimal network layer representation of sockets * @skc_daddr: Foreign IPv4 addr * @skc_rcv_saddr: Bound local IPv4 addr * @skc_dport: placeholder for inet_dport/tw_dport * @skc_num: placeholder for inet_num/tw_num * @skc_family: network address family * ... */ struct sock_common { __be32 skc_daddr; __be32 skc_rcv_saddr; ... } ``` 核心程式用 host endian 紀錄 port ,network endian 紀錄 ip,可以利用 `bpf_ntohs()` #### rtt 使用 bpf_tracing_net.h 中的 `tcp_sk()`,傳入 `sk` 呼叫得到 `struct tcp_sock *` 。 ```clike struct tcp_sock { u32 srtt_us; /* smoothed round trip time << 3 in usecs */` ... }; ``` 將 `srtt_us` 右移三位元就是 rtt 。因為 `struct tcp_sock` 是核心記憶體,得用 `BPF_CORE_READ` 讀取。 ### 測試範例 在本機可以透過命令列工具簡單製造 tcp 連線 ```shell # server $ python3 -m http.server # client $ curl 0.0.0.0:8000 ``` 比對輸出紀錄的 ip、port ,確認運作正常 ### tracepoint 實作說明 ![tcp](https://hackmd.io/_uploads/Sy-DmkxWyx.png) 三次交握時, socket 的狀態會改變。兩次“狀態改變”相隔的時間相當於 rtt ,也就是“從 **SYN_SENT** 到 **ESTABLISHED** 的時間”和“從 **SYN_RECV** 到 **ESTABLISHED** 的時間”,可以自行計算。 **inet_sock_set_state** 這個 tracepoint 在每次 socket 狀態切換時被觸發,再透過 map 紀錄同個 socket 上次觸發的時間,就可以在狀態變成 **ESTABLISHED** 的時候計算 rtt 。另外,**TCP_CLOSE** 代表連線關閉,可以刪除 map 中的紀錄。 ### tcprtt_tp.bpf.c 示範程式碼 ```clike #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #include <bpf/bpf_core_read.h> #include "tcprtt.h" #include "bpf_tracing_net.h" // TODO: define ring buffer // TODO: define hash map SEC("tracepoint/sock/inet_sock_set_state") int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx) { // handle ipv4 only if (ctx->family != AF_INET) return 0; // TODO: // if oldstate, newstate are desired states: // 蒐集 ip, port,計算 rtt // ring buffer 發送資料 // if newstate == TCP_CLOSE: // 刪除 map 紀錄 // else // 在 map 紀錄時間 return 0; } ``` ### 搜集資料 - ip 和 port:從 `struct trace_event_raw_inet_sock_set_state *ctx` 取得 - rtt:`bpf_ktime_get_ns()` 可以取得當前時間,map 紀錄上次觸發的時間,兩者相減得到 rtt #### 參數類別說明 ```clike struct trace_event_raw_inet_sock_set_state { ... const void *skaddr; int oldstate; int newstate; __u16 sport; __u16 dport; __u8 saddr[4]; __u8 daddr[4]; }; ``` - `skaddr`: 儲存 `struct sock` 的位址,可作為 hash map 的鍵 - `old_state`, `newstate`: socket 狀態,例如: **TCP_ESTABLISHED**, **TCP_SYN_SENT** ,定義在 vmlinux.h - `saddr`, `daddr`: ip,網路位元組順序,用 `bpf_core_read(dst, sz, src)` 讀取 - `sport`, `dport`: port,本機位元組順序 ### 目標二:練習項目 - 開發 eBPF 核心程式,在用戶輸出 TCP 相關資訊,包含 pid, command, 目標和來源的 ip 及 port 和延遲時間 1. 完成核心程式 **tcprtt.bpf.c**、**tcprtt_tp.bpf.c**。完成範例程式碼 TODO 部分 2. 編譯執行檔 **tcprtt**、**tcprtt_tp** 3. 執行並確認 ip、port - 比較同一筆連線兩種方式得到的數值是否相符 ## 參考資料 - [build eBPF with libbpf-bootstrap](https://nakryiko.com/posts/libbpf-bootstrap) - "Learning eBPF"--Liz Rice - [meet bpftool](https://qmonnet.github.io/whirl-offload/2021/09/23/bpftool-features-thread/)