eBPF笔记(四)—— eBPF的简单刨析
eBPF “Hello World” for a Network Interface
该程序在网络数据包到达时会写入一行跟踪
#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是向内核指示它应正常处理此数据包的标志。
- BCC 的版本称为 bpf_trace_printk(),libbpf 的版本是 bpf_printk(),但两者都是内核函数 bpf_trace_printk() 的封装。
Compiling an eBPF Object File
eBPF 源代码需要编译成 eBPF 虚拟机可以理解的机器指令:eBPF 字节码。如果指定 -target bpf,则 LLVM 项目中的 Clang 编译器将执行此操作。以下是将执行编译的 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 将程序加载到内核中:
$ bpftool prog load hello.bpf.o /sys/fs/bpf/hello
这会从我们编译的目标文件中加载 eBPF 程序,并将其“固定”到位置 /sys/fs/bpf/hello.4 对此命令没有输出响应表示成功。可以使用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(安全哈希算法)总和,可用作程序的另一个标识符。每次加载或卸载程序时,ID 可能会有所不同,但标记将保持不变。bpftool 实用程序接受按 ID、名称、tag或pinned path对 BPF 程序的引用,因此在此示例中,以下所有内容都将提供相同的输出
• bpftool prog show id 160
• bpftool prog show name hello
• bpftool prog show tag d35b94b4c0c10efb
• bpftool prog show pinned /sys/fs/bpf/hello
可以有多个具有相同名称的程序,甚至可以具有相同标记的多个程序实例,但 ID 和固定路径将始终是唯一的。
The Translated Bytecode
bytes_xlated 字段告诉我们有多少字节的“翻译”eBPF 代码。这是 eBPF 通过验证程序后的字节码
$ 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
翻译后的字节码是相当低级的,但还不是机器代码。eBPF 使用 JIT 编译器将 eBPF 字节码转换为在目标 CPU 上本机运行的机器代码。bytes_jited 字段显示,在此对话之后,程序的长度为 64 字节
为了获得更高的性能,eBPF 程序通常采用 JIT 编译。另一种方法是在运行时解释 eBPF 字节码。eBPF 指令集和寄存器被设计为与本机指令相当接近,使这种交互变得简单明了,因此速度相对较快,但编译程序会更快,而且大多数架构现在都支持 JIT.5
bpftool 实用程序可以用汇编语言生成此 JIT 代码的转储
# 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 程序附加到网络接口上的 XDP 事件,如下所示:
# bpftool net attach xdp id 160 dev ens33 这里我的网卡名称是ens33,因机器而异。
使用ip link 查看网络接口:
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:
此输出还提供了有关网络堆栈中其他一些潜在事件的一些线索,您可以将 eBPF 程序附加到这些事件:tc 和 flow_dissector
此时,hello eBPF 程序应该在每次收到网络数据包时生成跟踪输出。您可以通过运行 cat /sys/kernel/ debug/tracing/trace_pipe 来检查这一点。这应该显示许多类似于以下内容的输出:
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
这次没有与这些事件相关的命令或进程ID;相反,你在每行跟踪的开始看到的是
-0。在第2章中,每个系统调用事件都是因为一个在用户空间执行命令的进程调用了系统调用API。那个进程ID和命令是eBPF程序执行时的上下文的一部分。但在这里的例子中,XDP事件是由于网络包的到来而发生的。这个包没有关联的用户空间进程——在hello eBPF程序被触发的那一刻,系统除了将包接收到内存中外,还没有对其进行任何处理,而且它不知道这个包是什么或者它要去哪里。
Global Variables
正如你在前一章中了解到的,eBPF map是一种数据结构,可以从eBPF程序或用户空间访问。由于同一个map可以在同一程序的不同运行中重复访问,它可以用于保存一次执行到下一次执行的状态。多个程序也可以访问同一个map。由于这些特性,map语义可以重新用于作为全局变量。
在 2019 年添加对全局变量的支持之前,eBPF 程序语法必须显式编写map才能执行相同的任务。
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已经增长到了4238.
正如你将在第5章中学到的,bpftool 只有在BTF信息可用的情况下才能漂亮地打印出map的字段名称(这里是变量名称counter)。只有在编译时使用-g标志,才会包含这些信息。如果在编译步骤中省略了该标志,你将看到类似以下内容的输出:
$ bpftool map dump name hello.bss
key: 00 00 00 00 value: 8E 10 00 00
Found 1 element
map还用于保存静态数据:
root@wp-virtual-machine:~# bpftool map dump name hello.rodata
[{
"value": {
".rodata": [{
"hello.____fmt": "Hello World %d"
}
]
}
}
]
现在我们已经检查完这个程序及其maps,是时候清理它了。我们将从将其从触发事件中分离出来开始。
Detaching the Program
可以按照如下方法detach eBPF程序
root@wp-virtual-machine:~# bpftool net detach xdp dev ens33(注意你之前绑定的网卡名称,这里是我机器上的网卡名称ens33)
此时执行ip link 和bpftool net list 你将会发现已经成功分离。
但是,该程序仍加载到内核中:
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相对应的命令,但是你可以通过删除固定的伪文件来从内核中移除程序:
$ rm /sys/fs/bpf/hello
$ bpftool prog show name hello
BPF to BPF Calls
在前一章中,你看到了尾调用的示例,并且我提到现在也有能力在eBPF程序中调用函数。让我们看一个简单的例子,与尾调用示例类似,可以将其附加到sys_enter跟踪点,但这次它将跟踪系统调用的操作码。你会在第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";
以下函数从跟踪点参数中提取系统调用操作码:
static __attribute((noinline)) int get_opcode(struct bpf_raw_tracepoint_args *ctx) {
return ctx->args[1];
}
如果有选择的话,编译器可能会将这个非常简单的函数内联,而我只会从一个地方调用它。由于这样做会破坏这个例子的目的,我添加了__attribute((noinline))来强制编译器不进行内联。在正常情况下,你可能应该省略这一属性,让编译器根据需要进行优化。
调用此函数的 eBPF 函数如下所示:
SEC("raw_tp/")
int hello(struct bpf_raw_tracepoint_args *ctx) {
int opcode = get_opcode(ctx);
bpf_printk("Syscall: %d", opcode);
return 0;
}
make后执行如下命令
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程序调用了get_opcode()。偏移量为0的eBPF指令是0x85,根据指令集文档,对应着“函数调用”。执行不会继续执行下一条指令,而是会跳过七条指令(pc+7),也就是偏移量为8的指令。
函数调用指令需要将当前状态放在 eBPF 虚拟机的堆栈上,以便当被调用的函数退出时,可以在调用函数中继续执行。由于堆栈大小限制为 512 字节,因此 BPF 到 BPF 调用不能非常深入地嵌套。

浙公网安备 33010602011771号