# 第四講:透過 eBPF 觀察作業系統行為 > 本筆記僅為個人紀錄,相關教材之 Copyright 為[jserv](http://wiki.csie.ncku.edu.tw/User/jserv)及其他相關作者所有 * 直播:==[Linux 核心設計:透過 eBPF 觀察作業系統行為 - 2018/11/15](https://youtu.be/UmCnh6mELwA)== * 詳細共筆:[Linux 核心設計: 透過 eBPF 觀察作業系統行為](https://hackmd.io/@sysprog/linux-ebpf?type=view) * 主要參考資料: * [Linux BPF Superpowers](https://www.slideshare.net/slideshow/linux-bpf-superpowers/58986111) * [Meet cute-between-ebpf-and-tracing](https://www.slideshare.net/slideshow/meet-cutebetweenebpfandtracing/62446985) * [Kernel Analysis Using eBPF](https://events19.linuxfoundation.org/wp-content/uploads/2017/12/Kernel-Analysis-Using-eBPF-Daniel-Thompson-Linaro.pdf) --- 本課旨在闡述 Linux 核心的動態追蹤機制,特別是 eBPF (Extended Berkeley Packet Filter) 技術,以及如何利用它來觀察和分析作業系統的內部行為。 --- ## 前言:為何需要觀察作業系統行為 現代軟體系統面臨著前所未有的規模和複雜度挑戰。無論是使用者規模、機房規模還是軟體本身的層次 (作業系統核心、系統軟體、高階語言虛擬機、應用邏輯等),都在快速增長。在這樣的大規模生產環境中,詭異問題頻發,佔據工程人員大量時間。 **傳統的除錯方法**,如在程式碼中插入 `printk`,在**複雜的核心環境中可能導致行為改變甚至系統崩潰,且難以捕捉瞬態問題**。因此,需要更有效的機制來「活體分析」仍在運作並持續提供服務的軟體系統。 --- ## 核心動態追蹤機制 動態追蹤技術 (dynamic tracing) 是一種進階的除錯和追蹤機制,允許工程師以非常低的成本,在短時間內克服不明顯的問題。其核心思想是**將運作中的軟體系統視為一個即時變化的資料庫**,透過「探針」(probe) 進行查詢。 * **探針 (Probe)**:好比中醫針灸,在軟體系統的特定「穴位」 (如**函式入口、特定事件點**) 安置探針,探針上帶有自定義的「傳感器」 (**處理程式**),用於採集關鍵資訊。 * **非侵入式分析**:動態追蹤的關鍵優勢在於其非侵入性。如同醫生給病人拍 X 光或做核磁共振,而非直接開膛破肚。它**允許在不修改作業系統核心、應用程式或任何系統配置的情況下**,快速高效地獲取所需資訊。 若動態追蹤機制內建於作業系統,**使用者層級的程式**即可隨時採集資訊,構建完整的軟體樣貌。 主流的動態追蹤工具包括: * **DTrace**:源自 Solaris 作業系統。Oracle 已重新以 GNU GPL 釋出 DTrace 並嘗試整合到 Linux 核心。值得注意的是,[DTrace for Linux 2.0 已在 eBPF 的基礎上重新實作](https://lore.kernel.org/all/ZhBRSM2j0v7cOLn%2F@oracle.com/T/#u),不再依賴專屬的核心模組。 * **SystemTap**:Red Hat 的實作。 * **[ktap](https://github.com/ktap/ktap)**:華為科技的實作,使用 Lua 作為腳本語言。 > 延伸閱讀:[動態追蹤技術漫談](https://openresty.org/posts/dynamic-tracing/) --- ## eBPF 概覽 eBPF (Extended Berkeley Packet Filter) 最初源於 Berkeley Packet Filter (BPF),其設計初衷確實是**封包過濾機制**。然而,擴充後的 eBPF 已經演變為 Linux 核心內建的強大**內部行為分析工具**。 eBPF 的功能涵蓋: * 動態追蹤 (dynamic tracing)。 * 靜態追蹤 (static tracing)。 * 效能剖析事件 (profiling events)。 ![image alt](https://i.imgur.com/1HVZIzK.png) ### eBPF 在安全方面的應用 * **沙盒 (Sandbox) 環境**:eBPF 程式碼在核心內部的**沙盒虛擬機器**中執行,確保安全。 * **DDoS 防禦**:透過 [eXpress Data Path (XDP)](https://www.iovisor.org/technology/xdp),eBPF 可以在網路堆疊處理早期 (甚至在 `sk_buff` 分配前) 丟棄惡意封包,有效緩解 DDoS 攻擊。 * **入侵偵測系統 (Intrusion Detection System, IDS)**:eBPF 也可用於實現入侵偵測機制。 ### eBPF 的運作流程 使用者在使用者層級 (user mode) 準備 BPF 程式碼,用於測量延遲、堆疊追蹤等資訊。其運作方式如下: 1. **編譯**:BPF 程式碼在 user mode 被編譯成 BPF bytecode。 * Linux 核心 v5.2 之前:最多 4096條指令,512B 堆疊空間,核心內建 JIT (Just-In-Time) 編譯器。 * Linux 核心 v5.2 之後:指令數量上限放寬到 1 百萬條。 2. **載入與驗證**:BPF bytecode 被載入到核心後,會經過核心驗證器 (verifier) 的檢查。若驗證不通過 (例如,不安全或包含無限迴圈),則會被拒絕。 3. **掛載與執行**:若驗證通過,BPF 程式可以掛載到以下幾種不同的追蹤點執行: * **kprobes**:**核心**動態追蹤 (kernel dynamic tracing),用於探測核心函式的入口或特定指令。 * **uprobes**:**使用者層級**動態追蹤 (user level dynamic tracing),用於探測使用者態程式的函式。 * **tracepoints**:核心靜態追蹤 (kernel static tracing),由核心開發者**預先定義在程式碼中的追蹤點**,通常 ABI 較穩定。 * **perf\_events**:基於效能事件的取樣 (timed sampling and Performance Monitoring Counters, PMCs)。 ![image alt](https://i.imgur.com/i7b3DkP.png) 4. **資料回傳**:分析結果可以透過兩種主要方式傳回 user mode: * **Per-event details**:每個事件的詳細資料,通常透過 `perf` 事件的環形緩衝區 (ring buffer) 或 `bpf_trace_printk` (主要用於除錯,不建議在生產環境大量使用) 傳遞。 * **BPF maps**:一種通用的鍵值對 (key-value) 儲存結構,存在於核心空間,**user mode 和 BPF 程式都可以存取**。BPF maps 可以用於匯總數據,如製作直方圖、統計計數或相關性矩陣等。 ### 使用 eBPF 的先決條件 * 核心編譯選項 `CONFIG_BPF_SYSCALL` 必須啟用 (在`/proc/config.gz`)。 * 核心版本建議在 4.4 或以上,以獲得較完善的功能支援 (如直方圖、統計和追蹤每個事件的功能)。 ![image alt](https://i.imgur.com/4jgY4mj.png) (上圖$\color{green}{綠色}$字樣指該功能所需的最低核心版本) --- ## 作業系統的核心內建虛擬機器?! 從形式上說,BPF 就是一個核心內虛擬機器 (in-kernel virtual machine)。Linux 核心早在 2.1.75 版本 (1997年) 就已收錄 Berkeley Packet Filter (BPF) [JITC (A JIT for packet filters)](https://lwn.net/Articles/437981/)。 ![image](https://hackmd.io/_uploads/S1t1h0e-ex.png) ### `tcpdump` 與 BPF `tcpdump` 是一個常用的**網路封包分析工具**,它不僅能分析封包流向,還能「監聽」封包內容。其**底層實現嚴重依賴 BPF**。 當使用 `tcpdump` 並**指定過濾條件**時 (例如 `ip and udp`),這些條件會被編譯成 BPF bytecode,然後載入到核心中的 BPF 虛擬機器執行,以高效過濾封包。 ![image](https://hackmd.io/_uploads/Hyg1eJbblg.png) #### 範例:使用 `tcpdump` 觀察網路流量 以 IP 與 port number 捕捉 `eth0` 網路介面的封包: ```shell $ sudo tcpdump -i eth0 -nn listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes ... 22:02:05.803000 ARP, Request who-has 140.131.178.251 tell 140.131.179.254, length 46 22:06:46.922722 IP 140.131.178.244.22 > 110.50.189.175.57657: Flags [P.], seq 1502584330:1502584518, ack 1360901079, win 306, options [nop,nop,TS val 3241491854 ecr 590330642], length 188 ... ``` 輸出解讀 (需掌握[網路基礎](https://linux.vbird.org/linux_server/centos6/0110network_basic.php)概念): * `22:02:05.803000`:封包擷取時間。 * `ARP`:通訊協定為**位址解析協定** (Address Resolution Protocol)。 * ARP 利用乙太網路的**廣播特性**,動態查詢 IP 位址與實體位址 (MAC 位址) 的對應關係。 ![image alt](https://i.imgur.com/JKwqT9e.png) * `140.131.178.244.22 > 110.50.189.175.57657`:表示封包從 IP `140.131.178.244` 的 port `22` 發送到 IP `110.50.189.175` 的 port `57657`。`>` 指示傳輸方向。 * `[P.], seq 1502584330:1502584518`:TCP 旗標為 PUSH,表示帶有資料傳輸,序號範圍如示。 * `ack 1360901079`:確認號。 #### 查看 `tcpdump` 編譯的 BPF bytecode 使用 `-d` 選項可以將編譯後的封包匹配程式碼以**人類可讀**的形式傾印出來: ```shell $ sudo tcpdump -p -ni eth0 -d "ip and udp" (000) ldh [12] (001) jeq #0x800 jt 2 jf 5 (002) ldb [23] (003) jeq #0x11 jt 4 jf 5 (004) ret #262144 (005) ret #0 ``` 根據 [tcpdump(8)](https://linux.die.net/man/8/tcpdump) 手冊: * `-d`:傾印**編譯後**的封包匹配程式碼。 (開發者檢視) * `-dd`:將封包匹配程式碼傾印為 **C 語言** 結構陣列格式。 (嵌入 `libpcap` 函式庫) * `-ddd`:將封包匹配程式碼傾印為 **十進位格式** 的純數字陣列。 (機器處理或嵌入其他語言) * `jt`:jump if true。 * `jf`:jump if false。 上述 BPF bytecode 初步解讀: * `(000) ldh [12]`:從封包位移量 `12` (**乙太網路幀頭部**,表示**網路層**協定類型) 載入一個 half-word (2 bytes)。 * `(001) jeq #0x800 jt 2 jf 5`:檢查該值是否為 `0x0800` (IP 協定)。若是,跳到指令 `(002)`;否則跳到指令 `(005)` (返回 0,表示不匹配)。 * `(002) ldb [23]`:從封包位移量 `23` (**IP 協定頭部**,表示**傳輸層**協定類型) 載入一個 byte。 * `(003) jeq #0x11 jt 4 jf 5`:檢查該值是否為 `0x11` (UDP 協定)。若是,跳到指令 `(004)`;否則跳到指令 `(005)`。 * `(004) ret #262144`:返回非零值,表示**封包匹配**,擷取最多 `262144` bytes。 * `(005) ret #0`:返回 0,表示**封包不匹配**。 <!-- ![image alt](https://i.imgur.com/EnKGoYn.png) --> ![image](https://hackmd.io/_uploads/BJQP4AgZge.png) 使用 `-dd` 選項: ```shell $ sudo tcpdump -p -ni eth0 -dd "ip and udp" { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 3, 0x00000800 }, { 0x30, 0, 0, 0x00000017 }, { 0x15, 0, 1, 0x00000011 }, { 0x6, 0, 0, 0x00040000 }, { 0x6, 0, 0, 0x00000000 }, ``` 這段 C 陣列形式的程式碼可以被 `libpcap` 這樣的函式庫使用,並透過系統呼叫傳遞給核心內的 BPF 模組。 #### BPF 架構設計 位於核心的 BPF 模組是整個流程的核心:它接受 `tcpdump` 經由 `libpcap` 轉譯而來的過濾條件 (BPF bytecode),並將符合條件的封包訊息從核心模式複製到使用者層級,最終由 `libpcap` 發送給 `tcpdump`。 <!-- ![image](https://i.imgur.com/ZfmR4Tl.png) --> ![image](https://hackmd.io/_uploads/B1wCQRxbex.png) > 延伸閱讀:[Linux Socket Filtering aka Berkeley Packet Filter (BPF)](https://www.kernel.org/doc/Documentation/networking/filter.txt) ### BPF JIT (Just-In-Time) 編譯器 自 Linux 核心 v3.0 起,BPF 引入了 JIT 編譯器。若 `CONFIG_BPF_JIT` 編譯選項開啟,傳入的 BPF bytecode 會被編譯成平台相關的**本地機器碼**執行,以提高效率,替換掉預設的直譯器 `sk_run_filter()`。 ![image](https://hackmd.io/_uploads/rk2RvAlble.png) ![image](https://hackmd.io/_uploads/Bk3R0AeWge.png) * 可透過向 `/proc/sys/net/core/bpf_jit_enable` 寫入: * `1`:啟用 JIT。 * `2`:在核心訊息 (`dmesg`) 中看到 JIT 生成的最佳化程式碼。 * 可以使用核心工具 `bpf_jit_disasm.c` (位於 `tools/bpf/`) 將核心訊息中的**二進位碼**轉換為對應的**組合語言**。 * **安全問題**:Project Zero 曾[揭露 eBPF JIT 搭配 Spectre and Meltdown 漏洞發動攻擊的手段](https://googleprojectzero.blogspot.com/2018/01/reading-privileged-memory-with-side.html) ([中文解說](https://web.archive.org/web/20200205085508/https://look3little.blogspot.com/2018/01/spectre-and-meltdown.html))。這意味著如果 JIT 本身存在漏洞,攻擊者可能透過注入特製的 BPF 程式來利用硬體漏洞。 ### BPF 的演進:cBPF vs eBPF * **v3.4**:BPF 引入 [seccomp (secure computing mode)](http://man7.org/linux/man-pages/man2/seccomp.2.html),用於**限制行程可呼叫的系統呼叫**,增強安全性。 * **v3.14**:新增 `bpf_asm` (BPF 組譯器) 和 `bpf_dbg` (BPF 除錯器)。 * **v3.17**:引入 **extended BPF (eBPF)**。傳統的 BPF 被重命名為 classical BPF (cBPF)。 | 特性 | BPF | eBPF | | ----------- | ------------------------------------------------- | ------------------------------------------ | | registers | A, X | R0 ~ R10 | | width | 32 bit | 64 bit | | opcode | op:16, jt:8, jf:8, k:32 | op:8, dst_reg:4, src_reg:4, off:16, imm:32 | | JIT support | x86_64, SPARC, PowerPC, ARM, ARM64, MIPS, s390 | x86-64, aarch64, s390x | | Communication with kernel | `recv()` | maps | #### eBPF 帶來了巨大的改變: * **暫存器與指令集擴展**:eBPF 擁有更多的暫存器 (10個通用暫存器 + 1個唯讀幀指標),指令集也更通用,使其更像一個通用的 RISC CPU。 * **Map 機制**:cBPF 和核心溝通的機制主要是透過 `recv()` 系統呼叫複製封包資料。eBPF 引入了**全新的 `map` 機制**。 * **運作原理**:使用者層級的程式透過 `bpf(BPF_MAP_CREATE, ...)` 系統呼叫在核心中建立一個特定類型的 map (如 hash map, array map)。這個 map 是一個**鍵值對**儲存,**eBPF 程式**和**使用者層級程式**都可以存取它。 * **優勢**: * **效率**:顯著減少了核心態與使用者態之間的資料**複製和模式切換成本**。 * **資料多樣性**:Map 可以儲存**任意複雜的資料結構**,而不僅僅是封包。 ![image alt](https://i.imgur.com/XtkwqKY.png) * **輔助函式 (Helper Functions)**:eBPF 程式可以呼叫一組**預先定義的核心輔助函式**,以執行如讀取時間戳、存取 map、修改封包等操作,擴展了其能力。 * **主要應用範圍**: * cBPF:網路封包過濾、seccomp * eBPF:**核心追蹤**、**程式效能監控**、**流量控制 (traffic control)** ... 等 在 kernel space 處理 uprobe 觸發的成本,有機會比在 userspace 用 `SIGTRAP` 或 `ptrace` 處理 breakpoint 觸發還低。uprobe 觸發後,驅動 eBPF 執行環境,再用 eBPF map 將資料傳遞給使用者層級。 ### XDP (eXpress Data Path) XDP 允許 eBPF 程式在網路驅動程式的**極早期階段執行**,甚至在核心為封包分配 `sk_buff` 結構之前。這使得 XDP 在高速封包處理 (如 DDoS 防禦、負載平衡) 方面具有極高效能。 ![image](https://hackmd.io/_uploads/rkDJ0AlZlx.png) Cloudflare 的文章 [How to drop 10 million packets per second](https://blog.cloudflare.com/how-to-drop-10-million-packets/) 展示了其強大能力。 ![image](https://hackmd.io/_uploads/HkjEo0gWeg.png) > 延伸閱讀: > * [BPF - the forgotten bytecode](https://blog.cloudflare.com/bpf-the-forgotten-bytecode/) > * [eBPF 簡史](https://www.ibm.com/developerworks/cn/linux/l-lo-eBPF-history/) > * [eBPF, Sockets, Hop Distance and manually writing eBPF assembly](https://blog.cloudflare.com/epbf_sockets_hop_distance/) > * [Why is the kernel community replacing iptables with BPF?](https://cilium.io/blog/2018/04/17/why-is-the-kernel-community-replacing-iptables/) > * [History and Context of BPF](https://github.com/byoman/intro-ebpf/blob/master/notes.md) > 解說資料: > * [Introduction to Linux kernel tracing and eBPF integration](http://www.slideshare.net/vh21/meet-cutebetweenebpfandtracing) > * [Netronome: Demystify eBPF JIT Compiler](https://docs.google.com/document/d/1NL1TBK3yZIkqjVtCxxHOv6X_1j_LnZgi1cv4FBGEzFc/edit) > * [Introduction to eBPF and XDP 筆記](https://docs.google.com/document/d/19gf8C1sCcbPPlrL0RUMoaZvhPXg4j7B9y1SZKC38Td8/edit) --- ## eBPF 開發工具集 ### IO Visor 專案 2015 年,Linux 基金會成立了 [IO Visor](https://www.iovisor.org/) 專案,目標是實現高度彈性的資料平面 (data plane),以加速網路功能虛擬化 (NFV) 等。eBPF 是其關鍵軟體元件,允許在核心內部實作網路封包處理,避免繁瑣的系統呼叫和使用者層級資料處理。 [PLUMgrid](https://www.crunchbase.com/organization/plumgrid) (後被 VMware 收購) 是 eBPF 的重要貢獻公司,推動了 eBPF 相關成果整合到 Linux 核心。 ### BPF Compiler Collection (BCC) 儘管可用 C 語言編寫 BPF 程式,但直接處理編譯、解析 ELF 檔案、載入 BPF 程式碼塊以及建立 map 等操作對開發者而言較為繁瑣。 [BPF Compiler Collection (BCC)](https://github.com/iovisor/bcc) 作為 IOVisor 的子專案被提出,它是一個工具套件,極大簡化了 eBPF 程式的開發。 * **優點**:允許開發者**專注於用 C 語言編寫注入核心的邏輯**,其餘工作 (編譯、解析 ELF、載入 BPF 程式碼、建立 map 等) 大多**由 BCC 自動處理**。 * **語言使用**:BCC 通常與高階語言 (如 Python、Lua) 的綁定一起使用,開發者在高階語言腳本中**嵌入 C 語言的 eBPF 程式碼片段**。 ![image](https://hackmd.io/_uploads/Bye93SkZWgx.png) > 相關資源: > * **安裝:[官方說明文件](https://github.com/iovisor/bcc/blob/master/INSTALL.md)** > * [Writing eBPF tracing tools in Rust](https://jvns.ca/blog/2018/02/05/rust-bcc/) > * [Linux System Monitoring with eBPF](https://web.archive.org/web/20201109030908/https://www.circonus.com/2018/05/linux-system-monitoring-with-ebpf/) by Circonus > * [BPF metrics](https://github.com/circonus-labs/nad/tree/master/plugins/linux/bccbpf):Circonus 提供的插件,用於捕獲系統底層指標,如系統呼叫延遲直方圖、系統呼叫計數、塊設備延遲直方圖、排程器執行佇列延遲直方圖等。 > 解說:Kernel Analysis Using eBPF - Daniel Thompson, Linaro ([投影片](https://events.linuxfoundation.org/wp-content/uploads/2017/12/Kernel-Analysis-Using-eBPF-Daniel-Thompson-Linaro.pdf)/[影片](https://www.youtube.com/watch?v=AZTtTgni7LQ)) ### LLVM 與 Clang 在 eBPF 開發中的角色 直接撰寫 eBPF bytecode 並不容易。而 Clang (基於 LLVM 的 C/C++ 編譯器前端) 從 3.7 版本開始支援 BPF 後端,這意味著可以**將 C 語言程式碼編譯成 eBPF bytecode**。 * `a.c` ```c int f(int x){return x + 1;} ``` * **編譯 C 到 LLVM IR**: `$ clang -c -S -emit-llvm a.c` (產生 `a.ll`) * **編譯 C 到 eBPF 組合語言**: `$ clang -c -S -target bpf a.c` (產生 `a.s`) * **編譯 C 到 eBPF ELF 物件檔**: `$ clang -c -target bpf a.c` (產生 `a.o`) ```shell $ file a.o a.o: ELF 64-bit LSB relocatable, no machine, version 1 (SYSV), not stripped ``` * **從 ELF 物件檔中抽取 eBPF bytecode**: `$ objcopy -I elf64-little -O binary a.o a.bin` * **反組譯 eBPF bytecode (使用 [ubpf](https://github.com/iovisor/ubpf)-disassembler 工具)**: `$ ubpf-disassembler a.bin` --- ## eBPF 實際操作與範例 >[!Warning] 實驗前請確保安裝必要的套件 (以 Ubuntu 為例): > ```shell > $ sudo apt install -y linux-headers-$(uname -r) bpfcc-tools python3-bpfcc libbpfcc libbpfcc-dev > ``` > [在 WSL 的安裝方法](https://hackmd.io/@Jaychao2099/wsl-ebpf) Linux 的行程 (process) 和執行緒 (thread) 模型經歷了演變。早期 LinuxThreads 套件的實作將執行緒視為共享定址空間和資源的行程。NPTL (Native POSIX Thread Library) 的出現改進了這一點。 [clone(2)](http://man7.org/linux/man-pages/man2/clone.2.html) 系統呼叫是建立 new process 或 thread 的底層機制。它與 [fork(2)](http://man7.org/linux/man-pages/man2/fork.2.html) 的不同之處在於,`clone()` 建立的 child process 從指定的函式 (`fn` 參數) 開始執行,而 `fork()` 建立的 child process 從 `fork()` 呼叫點之後繼續執行。 以下透過 eBPF 追蹤 `clone()` 系統呼叫的使用狀況。 :::info #### 注意: 較新版本的 Linux 核心中,`sys_clone` 符號可能已變更 (例如變為 `__x64_sys_clone` 或其他架構特定的名稱,參考 [commit d5a0052](https://github.com/torvalds/linux/commit/d5a00528b58cdb2c71206e18bd021e34c4eab878))。可以使用以下命令檢查符號是否存在: ```shell $ cat /proc/kallsyms | grep sys_clone # 或者針對特定架構 $ cat /proc/kallsyms | grep __x64_sys_clone ``` 在後續的 Python 腳本中,`event="sys_clone"` 可能需要根據實際的核心符號進行調整。BCC 通常會嘗試自動解析,但明確指定有時是必要的。 ::: ### 範例 1:追蹤 `clone` 系統呼叫觸發 `hello.py`:每當 `clone` 系統呼叫觸發,就印出訊息。 ```python= #!/usr/bin/env python3 from bcc import BPF # eBPF program in C prog = """ int hello_world(void *ctx) { bpf_trace_printk("Hello, World! sys_clone called.\\n"); return 0; } """ # Load BPF program try: b = BPF(text=prog) b.attach_kprobe(event="sys_clone", fn_name="hello_world") except Exception as e: print(f"Error attaching kprobe: {e}") print("Try checking /proc/kallsyms for the correct clone syscall name (e.g., __x64_sys_clone).") exit() # Print messages from /sys/kernel/debug/tracing/trace_pipe print("Tracing clone syscalls... Press Ctrl-C to end.") try: # trace_print() 會持續讀取 trace_pipe # fmt 參數用於格式化輸出,{0} 代表行程名,{5} 代表 bpf_trace_printk 的訊息 b.trace_print(fmt="Program:{0} Message:{5}") except KeyboardInterrupt: pass ``` 執行時,每當有行程呼叫 `clone`,就會在 `/sys/kernel/debug/tracing/trace_pipe` 中看到 "Hello, World! sys_clone called." 的訊息。 ### 範例 2:統計 `clone` 系統呼叫次數 `count.py`:計算 `clone` 系統呼叫在一段時間內的總次數。 ```python= #!/usr/bin/env python3 from bcc import BPF import time # eBPF program prog = """ #include <uapi/linux/ptrace.h> // 定義一個名為 'stats' 的 BPF_ARRAY map // Key type: u32 (只有一個 key, index 0) // Leaf type: u32 (儲存記數值) // Map size: 1 element BPF_ARRAY(stats, u32, 1); int count_clone(struct pt_regs *ctx) { u32 key = 0; // 陣列只有一個元素, 所以 key 恆為 0 u32 *leaf = stats.lookup(&key); if (leaf) { (*leaf)++; // Increment the counter // bpf_trace_printk 用於偵錯, 可能會產生非常詳細的輸出 bpf_trace_printk("Total clone syscalls: %u\\n", *leaf); } return 0; } """ b = BPF(text=prog) try: # 自動取得 clone 的名稱: "sys_clone" 或 "__x64_sys_clone" 或 其他... syscall_fnname = b.get_syscall_fnname("clone") b.attach_kprobe(event=syscall_fnname, fn_name="count_clone") except Exception as e: print(f"Error attaching kprobe: {e}") exit() print("Counting clone syscalls... Press Ctrl-C to print results and exit.") # 初始化 map 中的 counter stats_map = b.get_table("stats") key = stats_map.Key(0) # Create a key object leaf = stats_map.Leaf(0) # Create a leaf object with initial value 0 stats_map[key] = leaf # Initialize the counter at index 0 to 0 try: while True: time.sleep(1) # 定期印出或僅等待 Ctrl+C current_val = stats_map[key].value print(f"Current clone count: {current_val}", end='\r') except KeyboardInterrupt: print("\nDetaching...") # 取得並印出最終計數 final_val = stats_map[key].value print(f"Total clone syscalls recorded: {final_val}") ``` 此範例使用了 `BPF_ARRAY` 類型的 map 來儲存計數。 ### 範例 3:計算 `clone` 系統呼叫的發生頻率 `freq.py`:計算 `clone` 系統呼叫在特定時間間隔內的發生頻率。 ```python= #!/usr/bin/env python3 from bcc import BPF from time import sleep # eBPF program prog = """ #include <uapi/linux/ptrace.h> // 名為 'stats' 的 BPF_ARRAY,包含一個用於計數的 u32 元素 BPF_ARRAY(stats, u32, 1); int increment_clone_count(struct pt_regs *ctx) { u32 key = 0; // 以原子方式遞增 counter stats.atomic_increment(key); // BCC 提供的 map 原子操作方法 return 0; } """ b = BPF(text=prog) try: # 自動取得 clone 的名稱: "sys_clone" 或 "__x64_sys_clone" 或 其他... syscall_fnname = b.get_syscall_fnname("clone") b.attach_kprobe(event=syscall_fnname, fn_name="increment_clone_count") except Exception as e: print(f"Error attaching kprobe: {e}") exit() print("Calculating clone syscall frequency (per second)... Press Ctrl-C to exit.") # 取得 BPF map stats_map = b.get_table("stats") interval_seconds = 1 iterations = 20 try: for i in range(iterations): # 在每個間隔開始時重設計數器 # 或者,更常見的是,讀取數值,然後減去前一個數值 # 方法一: 計算差值 prev_val = stats_map[stats_map.Key(0)].value if i > 0 else 0 sleep(interval_seconds) current_val = stats_map[stats_map.Key(0)].value rate = (current_val - prev_val) / interval_seconds print(f"Interval {i+1}: Total sys_clone per second = {rate:.2f} (Cumulative: {current_val})") # 方法二: 對於基於重置的簡單速率計算 # (對於隨時間變化的真實頻率而言不太準確): # stats_map[stats_map.Key(0)] = stats_map.Leaf(0) # 重設 counter # sleep(interval_seconds) # count_in_interval = stats_map[stats_map.Key(0)].value # print(f"Interval {i+1}: Total sys_clone per second = {count_in_interval / interval_seconds}") except KeyboardInterrupt: print("\nExiting...") ``` 此範例展示了如何定期讀取 map 中的計數值來計算頻率。更精確的頻率計算通常涉及讀取計數值的差量。 > 延伸閱讀: > * [Hist Triggers in Linux 4.7](http://www.brendangregg.com/blog/2016-06-08/linux-hist-triggers.html) (雖然與 eBPF 直接相關性不大,但提供了另一種基於事件的統計思路) > * [BPF Socket Filtering Demo](https://github.com/MatrixAI/Overwatch/tree/master/BPFSocketFilteringDemo):展示了 BPF 在 socket 層級過濾 UDP 封包的範例。 --- ## 總結 eBPF 是 Linux 核心中一項革命性的技術,它提供了一個安全、高效、靈活的框架,用於觀察、追蹤和甚至修改核心與使用者空間的行為。 透過其核心內的虛擬機器、驗證器和高效的 Maps 資料交換機制,eBPF 克服了傳統追蹤方法 (如 `printk`, `ptrace`) 的效能瓶頸和安全問題。 結合 BCC 等開發工具包,使得開發者能夠更容易地利用 eBPF 的強大能力來診斷效能問題、進行網路監控、增強安全性以及探索作業系統的內部運作,是理解和優化現代 Linux 系統不可或缺的工具。 --- ## 待整理/延伸學習 eBPF 是一個快速發展的領域,有大量資源和工具可供學習: * [eBPF - Understanding How It Works](https://blog.sofiane.cc/eBPFunderstanding/) * [Extended BPF:A New Type of Software](https://www.slideshare.net/brendangregg/um2019-bpf-a-new-type-of-software) / [video](https://youtu.be/7pmXdG8-7WU) * [BPF and formal verification](https://www.sccs.swarthmore.edu/users/16/mmcconv1/pl-reflection.html) * [From High Ceph Latency to Kernel Patch with eBPF/BCC](https://habr.com/en/company/selectel/blog/450818/) * [Extending Vector with eBPF to inspect host and container performance](https://medium.com/netflix-techblog/extending-vector-with-ebpf-to-inspect-host-and-container-performance-5da3af4c584b) * [lockstat using eBPF](https://github.com/prathyushpv/lockstat) * [eBPF can't count?!](https://blog.cloudflare.com/ebpf-cant-count/) (探討 eBPF map 在計數時的原子性與效能) * [Full-system dynamic tracing on Linux using eBPF and bpftrace](https://www.joyfulbikeshedding.com/blog/2019-01-31-full-system-dynamic-tracing-on-linux-using-ebpf-and-bpftrace.html) (bpftrace 是另一個基於 eBPF 的高階追蹤語言) * [awesome-ebpf](https://github.com/zoidbergwill/awesome-ebpf) (eBPF 相關資源列表) * [Extending the Kernel with eBPF (Android)](https://source.android.com/devices/architecture/kernel/bpf) * [eBPF Traffic Monitoring (Android)](https://source.android.com/devices/tech/datausage/ebpf-traffic-monitor) * [MicroBPF](https://github.com/alvenwong/MicroBPF):使用 BCC 探測核心的 TCP 指標和延遲。 * [eBPF Standard Documentation](https://github.com/ebpffoundation/ebpf-docs) (eBPF 基金會的官方文件) --- 回[主目錄](https://hackmd.io/@Jaychao2099/Linux-kernel) ---