---
tags: NCKU Linux Kernel Internals, 作業系統, eBPF
---
# Linux 核心設計: eBPF
[Linux 核心設計: 透過 eBPF 觀察作業系統行為](https://hackmd.io/@sysprog/linux-ebpf?type=view)
## 什麼是 BPF?
傳統的封包過濾方法,需要先將 kernel 的封包複製進 userspace,再進一步於 userspace 分析封包(使用 tap)。然而這個複製的過程 cost 是很高的。此外,傳統的封包過濾演算法效能也欠佳。
直到 1992 年 Steven McCanne 和 Van Jacobson 發表的論文 [The BSD Packet Filter: A New Architecture for User-level Packet Capture](https://www.tcpdump.org/papers/bpf-usenix93.pdf) 中,提出了 [Berkeley Packet Filter (BPF)](https://en.wikipedia.org/wiki/Berkeley_Packet_Filter),避免了把無用的封包複製到 userspace 的過程。形式上說來,BPF 就是一個跑在 kernel 中的虛擬機。因此,我們得以把 userspace 的程式轉換成 byte code(一個虛擬的指令集) 注入到 kernel space 中執行。
:::info
:laughing: 一些無聊的事:
這裡也可以看到 BPF 的原本應該是叫 BSD Packet Filter,也就是說機制原本是實現在 BSD 系統上的。不過似乎隨著 BPF 漸漸被不同作業系統納入,名稱就輾轉變成了 Berkeley Packet Filter 了 XD
:::
後來 Linux 也引入了 BPF,在某些應用上被使用(例如 [tcpdump](https://en.wikipedia.org/wiki/Tcpdump))。並且,BPF 也有了加速用的 Just-In-Time (jit) 編譯器。
之後,BPF 被擴展至網路以外的領域,而可以被應用在 kernel 效能分析,那便是 eBPF(extended BPF),而原本我們所說的 BPF 則被可以被改稱為 cBPF(classic BPF)
### BPF 的設計
BPF 的設計如圖。經過 [data link layer](https://en.wikipedia.org/wiki/Data_link_layer) 層的封包會額外傳遞一份給 BPF,在 kernel 先行過濾後再複製所要求的封包到 userspace。

除了在 kernel space 就先行過濾來提升效能以外,BPF 的 filter 架構也有學問。一般而言,filter 架構可以分成 boolean expression tree 或者 directed acyclic control flow graph(或稱 CFG) 兩種。在樹狀結構中,每個節點都表示一個 boolean operaiton(and、or 等,如下圖所示)

在 CFG 中,節點則是表示對一個封包欄位的判別(若為 true 就往右分支走,反之向左),並用兩個終點的 leaves 表示通過 filter 或否。(等價於前面 boolean expression tree 的 CFG 如下圖)

可以從圖中看到 CFG 會需要較少的 parsing 次數,因此 BPF 透過 CFG 的 filter 設計來提升效能。
:::info
BPF 的詳細請參照 reference,暫時只先研究部份內容。
另外,我感覺自己有些一知半解,因此如果有發現用詞不精準的地方都歡迎指教! :face_with_monocle:
:::
## 什麼是 eBPF?
eBPF 基於 BPF 可將使用者定義的程式注入 kernel 中的架構做出了更多改進,其設計得以利用現代硬體的優勢。
* eBPF virtual machine 更接近於現代的處理器,因此 eBPF 的虛擬指令集可以緊密地映射到真實硬體的 [ISA](https://en.wikipedia.org/wiki/Instruction_set_architecture),提高性能
* eBPF 使用 64 位元的暫存器,並且可用的暫存器數量也從 2 個提昇到 10 個
* cBPF 和核心溝通的機制是 recv(),而 eBPF 則引入全新的 map 機制。透過 map 達成 kernel 和 user 間的資料交換,減少 system call 耗費的時間成本
* 考量安全性,eBPF 有 verifier 機制,在注入程式之前,先進行一系列的檢查,避免注入危險的程序損壞到 kernel
其整體架構大致可以如下表示:

### eBPF 有何用途?
eBPF 理所當然的可以如 cBPF 做到過濾封包。然而實際上 eBPF 可以做得還遠遠不只這些! eBPF 可以把你的程式注入到 kernel 中,將其嵌入到任何的目標程式路徑上,一旦這些路徑被走過,便會執行注入的程式。因此 eBPF 可以做到:
* Kprobes (追蹤 kernel functions)
* Uprobes (追蹤 user functions)
* Linux Tracepoints
* USDT (Userland Statically Defined Tracing)
等等更多
### bpf system call
要在 linux 中實現 bpf,可以通過 [bpf()](https://man7.org/linux/man-pages/man2/bpf.2.html) 這個 system call
```c=
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
```
```c=
union bpf_attr {
struct { /* Used by BPF_MAP_CREATE */
__u32 map_type;
__u32 key_size; /* size of key in bytes */
__u32 value_size; /* size of value in bytes */
__u32 max_entries; /* maximum number of entries
in a map */
};
struct { /* Used by BPF_MAP_*_ELEM and BPF_MAP_GET_NEXT_KEY
commands */
__u32 map_fd;
__aligned_u64 key;
union {
__aligned_u64 value;
__aligned_u64 next_key;
};
__u64 flags;
};
struct { /* Used by BPF_PROG_LOAD */
__u32 prog_type;
__u32 insn_cnt;
__aligned_u64 insns; /* 'const struct bpf_insn *' */
__aligned_u64 license; /* 'const char *' */
__u32 log_level; /* verbosity level of verifier */
__u32 log_size; /* size of user buffer */
__aligned_u64 log_buf; /* user supplied 'char *'
buffer */
__u32 kern_version;
/* checked when prog_type=kprobe
(since Linux 4.1) */
};
} __attribute__((aligned(8)));
```
* attr 允許數據在 kernel 和 user space 間的交換
* cmd 決定 bpf 對應的相關操作
* size 是指向 attr 的 union 大小
在 linux/bpf.h 中有定義 cmd 的 macro
* BPF_PROG_LOAD
* BPF_MAP_CREATE
等等
### eBPF data structure
由於 eBPF 需要追蹤並統計 kernel 中的統計資訊,因此需要一個 eBPF Maps 資料結構來記錄。eBPF Maps 使用 **key-value** 的方式儲存數據, 可以透過 cmd = BPF_MAP_CREATE 建立,並得到一個 file descriptor 的回傳。
map 會有以下 member:
* map type(hash, array...)
* maximum number of elements
* key size in bytes
* value size in bytes
透過 map 機制,無論是從使用者層級抑或核心內部都可存取,進而提升了效率。
### 建立 eBPF bytecode
直接撰寫 eBPF 的虛擬指令(bytecode)並非易事,所幸 LLVM Clang compiler 支援將 C 編譯成 byte code,再透過 bfs system call(cmd = BPF_PROG_LOAD) 將 byte code 載入。你可以透過 Clang 的 `-target = bpf` 參數將 C 編成 elf 格式,再透過 libbpf 函式庫解析並注入 kernel。
在 [/samples/bpf](https://elixir.bootlin.com/linux/v4.12.6/source/samples/bpf) 中有許多範例程式。
## 透過 BCC 撰寫 BPF 程式碼
[BPF Compiler Collection (BCC)](https://github.com/iovisor/bcc) 作為 IOVisor 子計畫被提出,讓進行 BPF 的開發只需要專注於 C 語言注入於核心的邏輯和流程,剩下的工作,包括編譯、解析 ELF、載入 BPF 代碼塊以及建立 map 等等基本可以由 BCC 一力承擔,無需多勞開發者費心。
### 安裝
如果是 Ubuntu 可以使用下面指令,其他系統或者怕有問題最好直接參考 [Installing BCC](https://github.com/iovisor/bcc/blob/master/INSTALL.md)。此外,建議從 source 安裝,比較不會有版本太舊的問題。
```
git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
cmake ..
make
sudo make install
cmake -DPYTHON_CMD=python3 .. # build python3 binding
pushd src/python/
make
sudo make install
popd
```
也可以參考這位同學的[筆記](https://hackmd.io/@0xff07/HyrLVSZ78/https%3A%2F%2Fhackmd.io%2F%400xff07%2FBJ6vxInlU)
:::danger
紀錄一些途中有遇到到問題,不過基本上就是有缺套件,安裝一下就沒事了:
Unable to find clang libraries ->
> sudo apt-get install libclang-dev
Could NOT find LibElf ->
> sudo apt-get install libelf-dev
此外,**執行 python 檔/相關指令時要記得要用 sudo!** 如果忘記可能不會顯示權限不足而是其他問題。
:::
### 執行程式
#### `Kprobes 範例`
執行以下程式:
```c=
from bcc import BPF
prog = """
int hello(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="__x64_sys_clone", fn_name="hello")
print ("PID MESSAGE")
b.trace_print(fmt="{1} {5}")
```
:::warning
如果跟原本的程式相比,你會注意到 `attach_kprobe` 的 `event` 有做一些調整,這是因為原本的 `sys_clone` 在新的 Linux 版本中有[更換命名](https://github.com/torvalds/linux/commit/d5a00528b58cdb2c71206e18bd021e34c4eab878),如果照原本的寫法會找不到 symbol。
```
證據是,如果輸入
> sudo cat /boot/System.map-`uname -r` | grep " sys_clone"
會看到沒有東西印出來,而
> sudo cat /boot/System.map-`uname -r` | grep " __x64_sys_clone"
則會有相同的 symbol。(注意 sys_clone 有個空白XD)
```
或者透過以下指令看看是否存在 `sys_clone` 這個 kernel symbol
```
$ cat /proc/kallsyms | grep sys_clone
```
:::
程式的內容很淺顯易懂,就是每次 `__x64_sys_clone` 被執行的時候,印出 PID 跟 "Hello World" 訊息。
## CO-RE
見文 [BPF 的可移植性: Good Bye BCC! Hi CO-RE!](https://hackmd.io/@RinHizakura/HynIEOD7n)。
## 透過 libbpf 撰寫 BPF 程式碼
BCC 雖然一定程度簡化了 BPF 的開發,但使用上仍有一些缺點。例如可移植性的限制、高度依賴 Clang/LLVM 帶來的負擔等,因此社群會更建議透過使用 CO-RE 技術的 libbpf 來進行 BPF 程式的開發。
一般而言,使用 CO-RE 進行的 BPF program 可區分為兩部份。一部份會編譯成 BPF bytecode,之後被載入至 kernel space 運行;另一部份則是執行於 userspace 的 BPF loader。其依賴於 libbpf,被用來將 BPF bytecode 載入到 kernel,並監視其狀態以在 userspace 進行對應行為。
其中要載入至核心的部份只能用 C 撰寫( * ),更嚴謹的說是受限制的 C 語法。其透過可以支援編譯出 BPF bytecode 的編譯器編譯後,產生 ELF 格式的二進位檔。而執行在 user space 的程式則有較多選擇,可以用 C/Python/Rust 等等語言撰寫。
### 使用 C
可以參考以下文章
* [Develop a Hello World level eBPF program from scratch using C](https://www.sobyte.net/post/2022-07/c-ebpf/)
### 使用 Rust
在開始前,需要安裝一些 dependency:
```
$ apt install clang llvm libelf1 libelf-dev zlib1g-dev
```
此外也會需要 bpftool:
```
$ git clone https://github.com/libbpf/bpftool.git
$ cd bpftool
$ git submodule update --init
$ cd src
$ make
$ sudo make install
```
接著,假設我們要建立一個簡單專案 `hello`,可以遵循以下的步驟:
1. 建立專案的資料夾: `$ cargo new hello`
2. 在建立出來的 `hello` 底下的 `Cargo.toml` 中添加 [libbpf-cargo](https://crates.io/crates/libbpf-cargo) 和 [libbpf-rs ](https://crates.io/crates/libbpf-rs) 的 dependency
```
[dependencies]
libbpf-rs = "0.19"
[build-dependencies]
libbpf-cargo = "0.13"
```
3. 在 hello 底下建立檔案 `build.rs`(檔案名稱必須是 `build.rs`,參見 [The Cargo Book - Build Scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html))
```rust
use std::fs::create_dir_all;
use std::path::Path;
use libbpf_cargo::SkeletonBuilder;
const SRC: &str = "./src/bpf/hello.bpf.c";
/* Reference:
* - https://github.com/libbpf/libbpf-bootstrap/blob/master/examples/rust/tracecon/build.rs
*/
fn main() {
// FIXME: Is it possible to output to env!("OUT_DIR")?
std::env::set_var("BPF_OUT_DIR", "src/bpf/.output");
create_dir_all("src/bpf/.output").unwrap();
let skel = Path::new("src/bpf/.output/hello.skel.rs");
SkeletonBuilder::new()
.source(SRC)
.build_and_generate(&skel)
.expect("bpf compilation failed");
println!("cargo:rerun-if-changed={}", SRC);
}
```
4. 要被 hook 至核心的 BPF program 中總是要 include `vmlinux.h`,我們可以透過 `bpftool` 來產生,或者仿效 [libbpf-rs 的範例](https://github.com/libbpf/libbpf-rs/tree/master/examples/tproxy/src/bpf) 直接複製一份,但這作法可能須注意相容性的問題(?)
* [What is vmlinux.h and Why is It Important for Your eBPF Programs?](https://blog.aquasec.com/vmlinux.h-ebpf-programs)
* [聊聊對 BPF 程序至關重要的 vmlinux.h 文件](https://www.gushiciku.cn/pl/aEyZ/zh-hk)
```
bpftool btf dump file /sys/kernel/btf/vmlinux format c > src/bpf/vmlinux.h
```
5. 我們通過 C 來撰寫一個簡單的 BPF program `src/bpf/hello.bpf.c`:
* `SEC` 用來將每個編譯出的 object 放到給定的 ELF section 中,這會影響 bytecode 被載入 kernel 的行為,例如 `SEC("tracepoint/syscalls/sys_enter_execve")`,`tracepoint` 表示 BPF program 會追蹤 kernel 的特定事件,而具體的事件是 `syscalls/sys_enter_execve`,我們可以透過 `cat /sys/kernel/debug/tracing/available_events` 知道有哪些事件是可以追蹤的
```
$ sudo cat /sys/kernel/debug/tracing/available_events | grep sys_enter_execve
syscalls:sys_enter_execveat
syscalls:sys_enter_execve
```
* kernel 會提供 API 以供更容易的撰寫 BPF program,一系列的 API 在 [`tools/lib/bpf`](https://elixir.bootlin.com/linux/latest/source/tools/lib/bpf) 這個路徑底下,例如 [`bpf_printk`](https://elixir.bootlin.com/linux/latest/source/tools/lib/bpf/bpf_helpers.h#L287) 可以用來印出 debug message
```cpp
// We must include this
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
char msg[] = "Hello, World!";
bpf_printk("invoke bpf_prog: %s\n", msg);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
```
6. 執行 `cargo build`,之後我們應該可以在 `src/bpf/.output` 路徑中找到 `skel.rs`
7. 再來要準備 BPF loader 的部分,我們在 `src` 路徑下建立一個 `main.rs`,透過 `cargo build` 再編譯一次
```rust
use anyhow::Result;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::{thread, time};
#[path = "bpf/.output/hello.skel.rs"]
mod hello;
use hello::*;
/* Reference: https://www.sobyte.net/post/2022-07/c-ebpf/ */
fn main() -> Result<()> {
let hello_builder = HelloSkelBuilder::default();
/* Open BPF application */
let open_skel = hello_builder.open()?;
/* Load & verify BPF programs */
let mut skel = open_skel.load()?;
/* Attach tracepoint handler */
let _tracepoint = skel
.progs_mut()
.bpf_prog()
.attach_tracepoint("syscalls", "sys_enter_execve")?;
/* Run `sudo cat /sys/kernel/debug/tracing/trace_pipe` to
* see output of the BPF programs */
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
})?;
let ten_millis = time::Duration::from_millis(10);
while running.load(Ordering::SeqCst) {
/* trigger our BPF program */
eprint!(".");
thread::sleep(ten_millis);
}
Ok(())
}
```
8. 自此,我們終於可以載入 BPF bytcode 了,執行 `sudo ./target/debug/hello-libbpf-rs`(需要 root 權限因為其會需要 [`setrlimit`](https://linux.die.net/man/2/setrlimit))後,在另一個 shell 執行 `sudo cat /sys/kernel/debug/tracing/trace_pipe` 就可以找到我們注入的信息!
```
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
[sudo] password for rin:
sudo-11688 [004] d..31 1404.417057: bpf_trace_printk: invoke bpf_prog: Hello, World!
<...>-11690 [005] d..31 1408.100552: bpf_trace_printk: invoke bpf_prog: Hello, World!
^C
```
與 C 相比,使用 Rust 撰寫 eBPF code 的好處是得易於其完善的 build system,在專案管理上相對容易。例如 [`ebpf-strace`](https://github.com/RinHizakura/ebpf-strace) 這個專案只需藉由 cargo 就可以取得所有依賴的套件、完整編譯並運行。
## TODO
- [ ] [awesome-ebpf](https://github.com/zoidbergwill/awesome-ebpf)
- [ ] 透過 aya 撰寫 BPF 程式碼(in Rust)
## Reference
* [intro-ebpf](https://github.com/byoman/intro-ebpf/blob/master/notes.md)
* [Notes on BPF & eBPF](https://jvns.ca/blog/2017/06/28/notes-on-bpf---ebpf/)
* [Develop a Hello World level eBPF program from scratch using C](https://www.sobyte.net/post/2022-07/c-ebpf/)
* [Writing eBPF tracing tools in Rust](https://jvns.ca/blog/2018/02/05/rust-bcc/)
* [Writing BPF code in Rust](https://blog.redsift.com/labs/writing-bpf-code-in-rust/)
* [foniod/redbpf](https://github.com/foniod/redbpf)
* [aya-rs/aya](https://github.com/aya-rs/aya)
* [libbpf](https://libbpf.readthedocs.io/en/latest/index.html)
* [张亦鸣 : eBPF 简史](https://cloud.tencent.com/developer/article/1006317)
* [BPF, docs: libbpf Overview Document](https://lwn.net/Articles/925848/)
* [Libbpf: A Beginners Guide](https://www.containiq.com/post/libbpf)
* [ebpf/libbpf 程序使用 tracepoint 的常见问题](https://mozillazg.com/2022/05/ebpf-libbpf-tracepoint-common-questions.html)
* [BPF 进阶笔记](https://arthurchiao.art/blog/bpf-advanced-notes-1-zh/)
* [BPF ring buffer](https://nakryiko.com/posts/bpf-ringbuf/)
* [BPF CO-RE reference guide](https://nakryiko.com/posts/bpf-core-reference-guide/#bpf-core-read)