eBPF筆記(四)—— eBPF的簡單刨析
eBPF “Hello World” for a Network Interface
????該程序在網(wǎng)絡(luò)數(shù)據(jù)包到達(dá)時會寫入一行跟蹤
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
int counter = 0;
SEC("xdp")
int hello(struct xdp_md *ctx) {
bpf_printk("Hello World %d", counter);
counter++;
return XDP_PASS;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
- ????宏SEC()為ebpf程序定義了一個xdp類型
- ????返回值XPD_PASS是向內(nèi)核指示它應(yīng)正常處理此數(shù)據(jù)包的標(biāo)志。
- ????BCC 的版本稱為 bpf_trace_printk(),libbpf 的版本是 bpf_printk(),但兩者都是內(nèi)核函數(shù) bpf_trace_printk() 的封裝。
Compiling an eBPF Object File
????eBPF 源代碼需要編譯成 eBPF 虛擬機可以理解的機器指令:eBPF 字節(jié)碼。如果指定 -target bpf,則 LLVM 項目中的 Clang 編譯器將執(zhí)行此操作。以下是將執(zhí)行編譯的 Makefile 的摘錄:
hello.bpf.o: %.o: %.c
clang \
-target bpf \
-I/usr/include/$(shell uname -m)-linux-gnu \
-g \
-O2 -c $< -o $@
Inspecting an eBPF Object File
make后可以檢查一下生成的hello.bpf.o
$ file hello.bpf.o
hello.bpf.o: ELF 64-bit LSB relocatable, eBPF, version 1 (SYSV), with debug_info,
not stripped
This shows it’s an ELF (Executable and Linkable Format) file, containing eBPF code,
for a 64-bit platform with LSB (least significant bit) architecture. It includes debug
information if you used the -g flag at the compilation step.
Loading the Program into the Kernel
????在此示例中,我們將使用名為 bpftool 的實用程序。您還可以load programs programmatical.
????使用 bpftool 將程序加載到內(nèi)核中:
$ bpftool prog load hello.bpf.o /sys/fs/bpf/hello
????這會從我們編譯的目標(biāo)文件中加載 eBPF 程序,并將其“固定”到位置 /sys/fs/bpf/hello.4 對此命令沒有輸出響應(yīng)表示成功。可以使用ls /sys/fs/bpf來查看加載的bpf程序。
Inspecting the Loaded Program
????運行以下命令列出已被加載的ebpf程序列表:
$ bpftool prog list
.....
160: xdp name hello tag d35b94b4c0c10efb gpl
loaded_at 2024-05-19T19:22:03+0800 uid 0
xlated 96B jited 64B memlock 4096B map_ids 16,17
btf_id 109
????可以運行以下命令使用json格式輸出:
$ bpftool prog show id 160 --pretty
{
"id": 160,
"type": "xdp",
"name": "hello",
"tag": "d35b94b4c0c10efb",
"gpl_compatible": true,
"loaded_at": 1716117723,
"uid": 0,
"orphaned": false,
"bytes_xlated": 96,
"jited": true,
"bytes_jited": 64,
"bytes_memlock": 4096,
"map_ids": [16,17
],
"btf_id": 109
}
The BPF Program Tag
????tag是程序指令的 SHA(安全哈希算法)總和,可用作程序的另一個標(biāo)識符。每次加載或卸載程序時,ID 可能會有所不同,但標(biāo)記將保持不變。bpftool 實用程序接受按 ID、名稱、tag或pinned path對 BPF 程序的引用,因此在此示例中,以下所有內(nèi)容都將提供相同的輸出
? bpftool prog show id 160
? bpftool prog show name hello
? bpftool prog show tag d35b94b4c0c10efb
? bpftool prog show pinned /sys/fs/bpf/hello
????可以有多個具有相同名稱的程序,甚至可以具有相同標(biāo)記的多個程序?qū)嵗?ID 和固定路徑將始終是唯一的。
The Translated Bytecode
????bytes_xlated 字段告訴我們有多少字節(jié)的“翻譯”eBPF 代碼。這是 eBPF 通過驗證程序后的字節(jié)碼
$ bpftool prog dump xlated name hello
int hello(struct xdp_md * ctx):
; bpf_printk("Hello World %d", counter);
0: (18) r6 = map[id:165][0]+0
2: (61) r3 = *(u32 *)(r6 +0)
3: (18) r1 = map[id:166][0]+0
5: (b7) r2 = 15
6: (85) call bpf_trace_printk#-78032
; counter++;
7: (61) r1 = *(u32 *)(r6 +0)
8: (07) r1 += 1
9: (63) *(u32 *)(r6 +0) = r1
; return XDP_PASS;
10: (b7) r0 = 2
11: (95) exit
????這看起來與您之前在 llvm-objdump 的輸出中看到的反匯編代碼非常相似。偏移地址相同,指令看起來相似,例如,我們可以看到偏移量 5 處的指令為 r2=15。
The JIT-Compiled Machine Code
????翻譯后的字節(jié)碼是相當(dāng)?shù)图壍模€不是機器代碼。eBPF 使用 JIT 編譯器將 eBPF 字節(jié)碼轉(zhuǎn)換為在目標(biāo) CPU 上本機運行的機器代碼。bytes_jited 字段顯示,在此對話之后,程序的長度為 64 字節(jié)
為了獲得更高的性能,eBPF 程序通常采用 JIT 編譯。另一種方法是在運行時解釋 eBPF 字節(jié)碼。eBPF 指令集和寄存器被設(shè)計為與本機指令相當(dāng)接近,使這種交互變得簡單明了,因此速度相對較快,但編譯程序會更快,而且大多數(shù)架構(gòu)現(xiàn)在都支持 JIT.5
????bpftool 實用程序可以用匯編語言生成此 JIT 代碼的轉(zhuǎn)儲
# bpftool prog dump jited name hello
int hello(struct xdp_md * ctx):
bpf_prog_d35b94b4c0c10efb_hello:
; bpf_printk("Hello World %d", counter);
0: nopl (%rax,%rax)
5: nop
7: pushq %rbp
8: movq %rsp, %rbp
b: pushq %rbx
c: movabsq $-81287619428352, %rbx
16: movl (%rbx), %edx
19: movabsq $-110636984554736, %rdi
23: movl $15, %esi
28: callq 0xffffffffc9c90eb0
; counter++;
2d: movl (%rbx), %edi
30: addq $1, %rdi
34: movl %edi, (%rbx)
; return XDP_PASS;
37: movl $2, %eax
3c: popq %rbx
3d: leave
3e: retq
3f: int3
Attaching to an Event
????程序類型必須與它所附加的事件類型相匹配。在本例中,它是一個 XDP 程序,可以使用 bpftool 將示例 eBPF 程序附加到網(wǎng)絡(luò)接口上的 XDP 事件,如下所示:
# bpftool net attach xdp id 160 dev ens33 這里我的網(wǎng)卡名稱是ens33,因機器而異。
使用ip link 查看網(wǎng)絡(luò)接口:
root@wp-virtual-machine:~# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:fc:0c:b3 brd ff:ff:ff:ff:ff:ff
prog/xdp id 160 tag d35b94b4c0c10efb jited
altname enp2s1
你可以使用bpftool查看network-attached eBPF programs:
root@wp-virtual-machine:~# bpftool net list
xdp:
ens33(2) generic id 160
tc:
flow_dissector:
netfilter:
????此輸出還提供了有關(guān)網(wǎng)絡(luò)堆棧中其他一些潛在事件的一些線索,您可以將 eBPF 程序附加到這些事件:tc 和 flow_dissector
????此時,hello eBPF 程序應(yīng)該在每次收到網(wǎng)絡(luò)數(shù)據(jù)包時生成跟蹤輸出。您可以通過運行 cat /sys/kernel/ debug/tracing/trace_pipe 來檢查這一點。這應(yīng)該顯示許多類似于以下內(nèi)容的輸出:
wp@wp-virtual-machine:~$ sudo cat /sys/kernel/debug/tracing/trace_pipe
...
<idle>-0 [007] ..s21 21514.466911: bpf_trace_printk: Hello World 1741
<idle>-0 [007] ..s21 21514.513357: bpf_trace_printk: Hello World 1742
<idle>-0 [007] ..s21 21514.574762: bpf_trace_printk: Hello World 1743
<idle>-0 [007] ..s21 21514.620769: bpf_trace_printk: Hello World 1744
<idle>-0 [007] ..s21 21514.666429: bpf_trace_printk: Hello World 1745
<idle>-0 [007] ..s21 21514.712155: bpf_trace_printk: Hello World 1746
<idle>-0 [007] ..s21 21514.759256: bpf_trace_printk: Hello World 1747
<idle>-0 [007] ..s21 21514.767923: bpf_trace_printk: Hello World 1748
...
????如果您難以記住跟蹤管道的位置,可以使用命令 bpftool prog tracelog 獲得相同的輸出。
root@wp-virtual-machine:~# bpftool prog tracelog
這次沒有與這些事件相關(guān)的命令或進程ID;相反,你在每行跟蹤的開始看到的是
-0。在第2章中,每個系統(tǒng)調(diào)用事件都是因為一個在用戶空間執(zhí)行命令的進程調(diào)用了系統(tǒng)調(diào)用API。那個進程ID和命令是eBPF程序執(zhí)行時的上下文的一部分。但在這里的例子中,XDP事件是由于網(wǎng)絡(luò)包的到來而發(fā)生的。這個包沒有關(guān)聯(lián)的用戶空間進程——在hello eBPF程序被觸發(fā)的那一刻,系統(tǒng)除了將包接收到內(nèi)存中外,還沒有對其進行任何處理,而且它不知道這個包是什么或者它要去哪里。
Global Variables
正如你在前一章中了解到的,eBPF map是一種數(shù)據(jù)結(jié)構(gòu),可以從eBPF程序或用戶空間訪問。由于同一個map可以在同一程序的不同運行中重復(fù)訪問,它可以用于保存一次執(zhí)行到下一次執(zhí)行的狀態(tài)。多個程序也可以訪問同一個map。由于這些特性,map語義可以重新用于作為全局變量。
在 2019 年添加對全局變量的支持之前,eBPF 程序語法必須顯式編寫map才能執(zhí)行相同的任務(wù)。
root@wp-virtual-machine:~# bpftool map list
2: prog_array name hid_jmp_table flags 0x0
key 4B value 4B max_entries 1024 memlock 8512B
owner_prog_type tracing owner jited
3: hash flags 0x0
key 9B value 1B max_entries 500 memlock 59360B
16: array name hello.bss flags 0x400
key 4B value 4B max_entries 1 memlock 8192B
btf_id 109
17: array name hello.rodata flags 0x80
key 4B value 15B max_entries 1 memlock 336B
btf_id 109 frozen
32: array name libbpf_global flags 0x0
key 4B value 32B max_entries 1 memlock 352B
33: array name pid_iter.rodata flags 0x480
key 4B value 4B max_entries 1 memlock 8192B
btf_id 149 frozen
pids bpftool(22268)
34: array name libbpf_det_bind flags 0x0
key 4B value 32B max_entries 1 memlock 352B
????我們選擇hello.bss
root@wp-virtual-machine:~# bpftool map dump name hello.bss
[{
"value": {
".bss": [{
"counter": 4238
}
]
}
}
]
????可以看到counter已經(jīng)增長到了4238.
正如你將在第5章中學(xué)到的,bpftool 只有在BTF信息可用的情況下才能漂亮地打印出map的字段名稱(這里是變量名稱counter)。只有在編譯時使用-g標(biāo)志,才會包含這些信息。如果在編譯步驟中省略了該標(biāo)志,你將看到類似以下內(nèi)容的輸出:
$ bpftool map dump name hello.bss
key: 00 00 00 00 value: 8E 10 00 00
Found 1 element
????map還用于保存靜態(tài)數(shù)據(jù):
root@wp-virtual-machine:~# bpftool map dump name hello.rodata
[{
"value": {
".rodata": [{
"hello.____fmt": "Hello World %d"
}
]
}
}
]
????現(xiàn)在我們已經(jīng)檢查完這個程序及其maps,是時候清理它了。我們將從將其從觸發(fā)事件中分離出來開始。
Detaching the Program
可以按照如下方法detach eBPF程序
root@wp-virtual-machine:~# bpftool net detach xdp dev ens33(注意你之前綁定的網(wǎng)卡名稱,這里是我機器上的網(wǎng)卡名稱ens33)
此時執(zhí)行ip link 和bpftool net list 你將會發(fā)現(xiàn)已經(jīng)成功分離。
但是,該程序仍加載到內(nèi)核中:
root@wp-virtual-machine:~# bpftool prog show name hello
160: xdp name hello tag d35b94b4c0c10efb gpl
loaded_at 2024-05-19T19:22:03+0800 uid 0
xlated 96B jited 64B memlock 4096B map_ids 16,17
btf_id 109
Unloading the Program
目前(至少在撰寫本文時)沒有與bpftool prog load相對應(yīng)的命令,但是你可以通過刪除固定的偽文件來從內(nèi)核中移除程序:
$ rm /sys/fs/bpf/hello
$ bpftool prog show name hello
BPF to BPF Calls
在前一章中,你看到了尾調(diào)用的示例,并且我提到現(xiàn)在也有能力在eBPF程序中調(diào)用函數(shù)。讓我們看一個簡單的例子,與尾調(diào)用示例類似,可以將其附加到sys_enter跟蹤點,但這次它將跟蹤系統(tǒng)調(diào)用的操作碼。你會在第3章的hello-func.bpf.c中找到這段代碼。
????完整代碼:
點擊查看完整代碼代碼
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
static __attribute((noinline)) int get_opcode(struct bpf_raw_tracepoint_args *ctx) {
return ctx->args[1];
}
SEC("raw_tp/")
int hello(struct bpf_raw_tracepoint_args *ctx) {
int opcode = get_opcode(ctx);
bpf_printk("Syscall: %d", opcode);
return 0;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
????以下函數(shù)從跟蹤點參數(shù)中提取系統(tǒng)調(diào)用操作碼:
static __attribute((noinline)) int get_opcode(struct bpf_raw_tracepoint_args *ctx) {
return ctx->args[1];
}
如果有選擇的話,編譯器可能會將這個非常簡單的函數(shù)內(nèi)聯(lián),而我只會從一個地方調(diào)用它。由于這樣做會破壞這個例子的目的,我添加了__attribute((noinline))來強制編譯器不進行內(nèi)聯(lián)。在正常情況下,你可能應(yīng)該省略這一屬性,讓編譯器根據(jù)需要進行優(yōu)化。
????調(diào)用此函數(shù)的 eBPF 函數(shù)如下所示:
SEC("raw_tp/")
int hello(struct bpf_raw_tracepoint_args *ctx) {
int opcode = get_opcode(ctx);
bpf_printk("Syscall: %d", opcode);
return 0;
}
make后執(zhí)行如下命令
bpftool prog load hello-func.bpf.o /sys/fs/bpf/hello
查看程序:
root@wp-virtual-machine:~# bpftool prog list name hello
216: raw_tracepoint name hello tag 3d9eb0c23d4ab186 gpl
loaded_at 2024-05-19T20:37:14+0800 uid 0
xlated 80B jited 62B memlock 4096B map_ids 45
btf_id 179
$ bpftool prog dump xlated name hello
int hello(struct bpf_raw_tracepoint_args * ctx):
; int opcode = get_opcode(ctx);
0: (85) call pc+7#bpf_prog_cbacc90865b1b9a5_get_opcode
; bpf_printk("Syscall: %d", opcode);
1: (18) r1 = map[id:193][0]+0
3: (b7) r2 = 12
4: (bf) r3 = r0
5: (85) call bpf_trace_printk#-73584
; return 0;
6: (b7) r0 = 0
7: (95) exit
int get_opcode(struct bpf_raw_tracepoint_args * ctx):
; return ctx->args[1];
8: (79) r0 = *(u64 *)(r1 +8)
; return ctx->args[1];
9: (95) exit
在這里,你可以看到hello() eBPF程序調(diào)用了get_opcode()。偏移量為0的eBPF指令是0x85,根據(jù)指令集文檔,對應(yīng)著“函數(shù)調(diào)用”。執(zhí)行不會繼續(xù)執(zhí)行下一條指令,而是會跳過七條指令(pc+7),也就是偏移量為8的指令。
函數(shù)調(diào)用指令需要將當(dāng)前狀態(tài)放在 eBPF 虛擬機的堆棧上,以便當(dāng)被調(diào)用的函數(shù)退出時,可以在調(diào)用函數(shù)中繼續(xù)執(zhí)行。由于堆棧大小限制為 512 字節(jié),因此 BPF 到 BPF 調(diào)用不能非常深入地嵌套。

浙公網(wǎng)安備 33010602011771號