eBPF 运行原理和流程
前言
极客时间 eBPF 核心技术与实战 的学习笔记.
本章说一下 ebpf 的运行原理, 本章有些内容是直接 copy 自课程原文
eBPF虚拟机(执行器)包含了什么
官方的说话, eBPF 是运行在 eBPF 虚拟机中, 而不是直接作用于系统.
但是也有人说, eBPF执行系统更应该称之为 执行器, 因为他并不如 虚拟机, 他们的差异如下:
- eBPF只提供了有限的指令集, 并不像虚拟机一样是一个完整的系统, 提供了完整的指令集, 这是因为 eBPF 不能影响到系统的稳定性, 所以只提供了一些处理过的指令集.
在前一章中, 我们可以发现内核态代码部分, 使用 C 直接调用了辅助函数, 这是 eBPF 为了提高开发效率有意为之的.
那么我们看一下 eBPF 虚拟机包含了哪些部分
eBPF辅助函数
就像上一章的 bpf_get_current_pid_tgid()
, eBPF为我们提供了许多辅助函数, 通过这些辅助函数来调用到系统的若干运行信息
这些函数实际上是帮我们调用了内核的其他模块, 但是需要注意的是, eBPF提供的辅助函数并不是全部可用的, 能够调用的函数由 eBPF 的程序类型决定
eBPF验证器
用于确保 eBPF 代码的安全, 验证器会将需要执行的指令创建成有向无环图, 确保执行的指令都是可达的, 再模拟执行指令, 确保指令不是无效的
存储模块
11 个 64 位寄存器、一个程序计数器和一个 512 字节的栈组成的存储模块. 这里控制eBPF程序的执行. 这样的设计, 导致了程序的若干限制:
- 函数的调用只能有一个返回值
- 函数调用参数不能超过5个
- 栈存储不能超过512字节
即时编译器
将 eBPF 程序编译成字节码执行
BPF 映射(map)
大块存储, 可以让用户态程序访问, 来进行数据的读取
BPF指令长什么样
需要先安装 bpftool
apt install bpftool
然后运行
bpftool prog list
会输出当前运行的 bpf 程序, 打印类似于
root@VM-4-12-debian:~# bpftool prog list
3: cgroup_device name sd_devices tag 3650d9673c54ce30 gpl
loaded_at 2024-11-30T14:06:29+0800 uid 0
xlated 504B jited 310B memlock 4096B
其中, 3 是这个 eBPF 程序的编号, cgroup_device 是这个程序的类型, sd_devices 是这个 程序的名字
我们可以再开一个命令行, 运行上一章的 helloworld.py, 在运行时再次运行 bpftool prog list
42: kprobe name hello_world tag 38dd440716c4900f gpl
loaded_at 2024-12-04T21:12:45+0800 uid 0
xlated 104B jited 71B memlock 4096B
btf_id 85
发现这次多了一条, 名称就是我们定义的 hello_world, 而我们的程序类型是 kprobe
, 编号是 42, 知道了 编号 后, 我们可以查看这个程序的详细指令(42注意替换为你的 编号)
root@VM-4-12-debian:~# sudo bpftool prog dump xlated id 42
int hello_world(void * ctx):
; int hello_world(void *ctx)
0: (b7) r1 = 33
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
1: (6b) *(u16 *)(r10 -4) = r1
2: (b7) r1 = 1684828783
3: (63) *(u32 *)(r10 -8) = r1
4: (18) r1 = 0x57202c6f6c6c6548
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
;
8: (07) r1 += -16
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
9: (b7) r2 = 14
10: (85) call bpf_trace_printk#-61424
; return 0;
11: (b7) r0 = 0
12: (95) exit
其中, ;
开头的行是我们编写的代码, 其他行是转换的指令
就拿 0: (b7) r1 = 33
举例, 0
是指令的行数, (b7)
是十六进制值, 代表BPF 指令码, 具体的码和含义可以参考 bpf-docs/eBPF.md at master · iovisor/bpf-docs , 这里的 b7
代表 64位寄存器赋值, r1 = 33
则是BPF 指令的伪代码
所以上面的详细指令可以翻译成:
- 第 0-8 行,借助 R10 寄存器从栈中把字符串 “Hello, World!” 读出来,并放入 R1 寄存器中
- 第 9 行,向 R2 寄存器写入字符串的长度 14(即代码注释里面的
sizeof(_fmt)
) - 第 10 行,调用 BPF 辅助函数 bpf_trace_printk 输出字符串
- 第 11 行,向 R0 寄存器写入 0,表示程序的返回值是 0
- 最后一行,程序执行成功退出
这些指令先通过 R1 和 R2 寄存器设置了 bpf_trace_printk 的参数, 然后调用 bpf_trace_printk 函数输出字符串, 最后再通过 R0 寄存器返回成功.
而BPF 虚拟机在接受到这些指令后, 经过校验, 会通过即时编译器模块编译成本地机器指令执行.
使用命令查看编译后的本地机器指令
bpftool prog dump jited id 42
如果报错说不支持, 是因为内核默认不开启查看机器指令的功能, 可自行搜索解决办法
BPF程序什么时候执行
需要安装模块 strace
apt install strace
使用 strace 工具来查看 hello.py 的运行过程
# -ebpf表示只跟踪bpf系统调用
sudo strace -v -f -ebpf ./hello.py
输出
bpf(BPF_PROG_LOAD,
{
prog_type=BPF_PROG_TYPE_KPROBE,
insn_cnt=13,
insns=[
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x21},
{code=BPF_STX|BPF_H|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-4, imm=0},
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x646c726f},
{code=BPF_STX|BPF_W|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-8, imm=0},
{code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x6c6c6548},
{code=BPF_LD|BPF_W|BPF_IMM, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x57202c6f},
{code=BPF_STX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-16, imm=0},
{code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_10, off=0, imm=0},
{code=BPF_ALU64|BPF_K|BPF_ADD, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0xfffffff0},
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_2, src_reg=BPF_REG_0, off=0, imm=0xe},
{code=BPF_JMP|BPF_K|BPF_CALL, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x6},
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0},
{code=BPF_JMP|BPF_K|BPF_EXIT, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}
],
prog_name="hello_world",
...
},
128) = 4
可以看到调用 bpf
函数, 传入了 3 个参数, 实际上 bpf
函数只需要3个参数, 那么这里,这三个参数的含义是:
- 第一个参数是 BPF_PROG_LOAD, 表示加载 BPF 程序
- 第二个参数是 bpf_attr 类型的结构体, 表示 BPF 程序的属性. 其中, 有几个需要你留意的参数, 比如: prog_type 表示 BPF 程序的类型, 是 BPF_PROG_TYPE_KPROBE , 跟我们 Python 代码中的 attach_kprobe 一致. insn_cnt (instructions count) 表示指令条数, insns (instructions) 包含了具体的每一条指令, 这儿的 13 条指令跟我们前面 bpftool prog dump 的结果是一致的
- prog_name 则表示 BPF 程序的名字, 即 hello_world. 第三个参数 128 表示属性的大小.
而在第一章中, 我们就说了, eBPF程序并不是一直运行, 而是指定的事件发生后才触发执行.
我们的 hello.py
中代码写明了调用了 attach_kprobe
进行事件的注册
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
为了验证这个结果, 我们使用 strace 再次获取一下, 这次获取全部的流程而不是只是 ebpf
strace -v -f ./hello.py
会发现调用如下
...
/* 1) 加载BPF程序 */
bpf(BPF_PROG_LOAD,...) = 4
...
/* 2)查询事件类型 */
openat(AT_FDCWD, "/sys/bus/event_source/devices/kprobe/type", O_RDONLY) = 5
read(5, "6\n", 4096) = 2
close(5) = 0
...
/* 3)创建性能监控事件 */
perf_event_open(
{
type=0x6 /* PERF_TYPE_??? */,
size=PERF_ATTR_SIZE_VER7,
...
wakeup_events=1,
config1=0x7f275d195c50,
...
},
-1,
0,
-1,
PERF_FLAG_FD_CLOEXEC) = 5
/* 4)绑定BPF到kprobe事件 */
ioctl(5, PERF_EVENT_IOC_SET_BPF, 4) = 0
...
所以, 其实eBPF 的程序执行分为如下几步:
- 借助 bpf 系统调用,加载 BPF 程序,并记住返回的文件描述符
- 查询 kprobe 类型的事件编号。BCC 实际上是通过 /sys/bus/event_source/devices/kprobe/type 来查询的
- 调用 perf_event_open 创建性能监控事件。比如,事件类型(type 是上一步查询到的 6)、事件的参数( config1 包含了内核函数 do_sys_openat2 )等
- 通过 ioctl 的 PERF_EVENT_IOC_SET_BPF 命令,将 BPF 程序绑定到性能监控事件。