# 第四講:透過 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)。

### 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)。

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 或以上,以獲得較完善的功能支援 (如直方圖、統計和追蹤每個事件的功能)。

(上圖$\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/)。

### `tcpdump` 與 BPF
`tcpdump` 是一個常用的**網路封包分析工具**,它不僅能分析封包流向,還能「監聽」封包內容。其**底層實現嚴重依賴 BPF**。
當使用 `tcpdump` 並**指定過濾條件**時 (例如 `ip and udp`),這些條件會被編譯成 BPF bytecode,然後載入到核心中的 BPF 虛擬機器執行,以高效過濾封包。

#### 範例:使用 `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 位址) 的對應關係。

* `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,表示**封包不匹配**。
<!--  -->

使用 `-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`。
<!--  -->

> 延伸閱讀:[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()`。


* 可透過向 `/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 可以儲存**任意複雜的資料結構**,而不僅僅是封包。

* **輔助函式 (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 防禦、負載平衡) 方面具有極高效能。

Cloudflare 的文章 [How to drop 10 million packets per second](https://blog.cloudflare.com/how-to-drop-10-million-packets/) 展示了其強大能力。

> 延伸閱讀:
> * [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 程式碼片段**。

> 相關資源:
> * **安裝:[官方說明文件](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)
---