# 虛擬化-lab1
## eBPF 介紹
屬於核心(kernel)程式開發。與核心模組主要差別在**驗證器**(verifier)和 **map** 的設計。驗證器由 Linux kernel 提供,確認被加載的程式的正確性。map 則提供便利的通訊介面,讓 BPF 核心程式間和用戶程式能夠輕易的協作。
### 追蹤程式介紹
eBPF 程式的兩個目的:觀測和改變核心行為,用於觀測的程式可以歸類為追蹤(trace)的類別。
追蹤程式底下可以再分成 kprobe/kretprobe, fentry/fexit 和 tracepoint :
- kprobe/kretprobe, fentry/fexit: 主要觀察核心中的函式的進入和退出,可以取得呼叫函式的引數(arguments)和回傳值。被追蹤的程式無須修改和重新編譯,屬於動態追蹤的技術。
- tracepoint: 核心程式碼設立和提供 tracepoint 以供觀測,屬於靜態追蹤。這套機制由 Linux 提供,屬於穩定的公開介面。
### 系統架構
系統元件圖

- `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 編譯流程

### 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 實作說明

三次交握時, 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/)