基础概念
- 内核虚拟机技术,通过sys_bpf加载到内核中,绑定到事件执行
- 访问内核数据结构和API,如网络数据包、系统调用、内核跟踪点,对数据进行过滤、修改、分析
- 安全性和可验证性,可通过Verifier进行严格检查
技术原理和实现
- 字节码,JIT编译器优化,转化为机器码
- 指令集是虚拟机的执行单元,并提供辅助函数来完成内存访问、系统调用和跟踪等
- 传统虚拟机提供用户程序的运行环境,ebpf虚拟机用于增强内核功能
eBPF程序剖析
事件和钩子
- 系统调用:当用户空间程序通过系统调用执行内核功能时
- 功能的进入和退出:在函数退出之前拦截调用
- 网络事件:当接收到数据包时
- kprobe和uprobe中:挂载到内核或用户函数中
辅助函数
让ebpf访问内存的丰富功能,如Helper:
- 在数据表中对键值对进行搜索、更新和删除
- 生成伪随机数
- 搜集和标记隧道元数据
- 把ebpf程序连接起来,也叫tail call
- 执行socket相关任务,如绑定、获取cookie、数据包重定向等
map
- 在ebpf程序和内核、用户空间之间存储和共享数据,ebpf程序通过辅助函数在map中发送和接收数据
应用场景
- 网络数据包过滤和流量控制:在网络驱动程序中加载ebpf程序
- 系统性能分析和调优:关键代码路径中插入ebpf程序,实时收集系统运行情况和性能指标
- 安全监控和防护:加载ebpf程序到内核中,实时捕获和分析系统行为
发展前景
- 社区发展和生态系统
- 云原生和容器技术中的应用
- 挑战和未来发展:安全和性能平衡、开发和调试工具等
编码实践
无论eBPF程序的用户态部分是用什么语言开发,内核态部分都还是需要用C语言开发。本例使用Go语言开发用户态部分,使用社区目前最为活跃的cilium/ebpf包。
环境安装
在Debian中使用如下命令安装 clang/llvm
编译器。
$ sudo apt-get update -y
$ sudo apt-get install -y llvm
$ sudo apt-get install -y clang
以及还需要安装 docker
运行环境。
下载 cilium/ebpf
库:
$ git get github.com/cilium/ebpf
在examples目录下有几个ebpf示例程序可以参考。
cilium/ebpf
提供了便利的构建脚本,我们只需在 ebpf/examples 下面执行 make -C ..
即可进行示例代码的构建。
示例分析
我们可以看一下 tracepoint_in_c
例子,其中包含以下几个文件:
$ tree tracepoint_in_c/
tracepoint_in_c/
├── bpf_bpfeb.go
├── bpf_bpfeb.o
├── bpf_bpfel.go
├── bpf_bpfel.o
├── main.go
└── tracepoint.c
1 directory, 6 files
其中C语言文件 tracepoint.c
就是ebpf程序内核态部分,bpf_bpfeb.o
和 bpf_bpfel.o
是编译后的bpf字节码文件,bpf_bpfel.go
和 bpf_bpfeb.go
则是bpf2go基于字节码生成,提供给用户态程序 main.go 使用。
bpf_bpfeb.o
和 bpf_bpfel.o
分别对应大小端不同的CPU架构,后缀 _bpfeb
中的 eb
代表 Big Ending
,同理 el
代表 Little Ending
。bpf_bpfel.go
/ bpf_bpfeb.go
文件也是如此,所以最终根据具体的CPU架构只会使用二中其一。
比如因为X86的CPU都是小端所以使用 bpf_bpfel.o 和 bpf_bpfel.go 即可,bpf_bpfel.go 与 main.go 一起编译就形成了ebpf程序。ebpf程序的字节码 bpf_bpfel.o 会以二进制数据的形式内嵌到这两个go源文件中,具体说就是利用 go:embed
特性嵌到 _BpfBytes
变量中。
可在bpf2go生成的bpf_bpfel.go文件的末尾的找到以下内容:
//go:embed bpf_bpfel.o
var _BpfBytes []byte
bpf_bpfel.go文件中也包含一个重要的结构 bpfObjects
,对应eBPF程序的两个关键组成:eBPF程序数据、用于用户态和内核态数据交互的map。eBPF程序还有一个关键组成就是 挂载点(attach point)。
// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
bpfPrograms
bpfMaps
}
操作实践
以 kprobe
为例,统计系统调用 sys_execve
被调用的次数,使用 map
传递数据。在 examples 目录下新建一个 kprobe_count 示例目录,C源码:
#include "common.h"
char __license[] SEC("license") = "Dual MIT/GPL";
struct bpf_map_def SEC("maps") kprobe_map = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(u32),
.value_size = sizeof(u64),
.max_entries = 1,
};
SEC("kprobe/sys_execve")
int kprobe_execve() {
u32 key = 0;
u64 initval = 1, *valp;
valp = bpf_map_lookup_elem(&kprobe_map, &key);
if (!valp) {
bpf_map_update_elem(&kprobe_map, &key, &initval, BPF_ANY);
return 0;
}
__sync_fetch_and_add(valp, 1);
return 0;
}
其中第一行的 common.h
是 cilium/ebpf 提供的开发ebpf程序用户态部分所需的头文件,在 examples/headers
目录中。
$ ls kprobe_count/
kprobe.c
1. bpf2go 内核态程序代码编译
使用 bpf2go
根据上面的C代码生成bpf字节码和Go代码,bpf2go执行中会调用到 clang/llvm
。
$ GOPACKAGE=main /home/caodailiang/go/bin/bpf2go -cc clang-14 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf kprobe.c -- -I /home/caodailiang/go/src/github.com/cilium/ebpf/examples/headers
Compiled /home/caodailiang/go/src/github.com/cilium/ebpf/examples/kprobe_count/bpf_bpfel.o
Stripped /home/caodailiang/go/src/github.com/cilium/ebpf/examples/kprobe_count/bpf_bpfel.o
Wrote /home/caodailiang/go/src/github.com/cilium/ebpf/examples/kprobe_count/bpf_bpfel.go
Compiled /home/caodailiang/go/src/github.com/cilium/ebpf/examples/kprobe_count/bpf_bpfeb.o
Stripped /home/caodailiang/go/src/github.com/cilium/ebpf/examples/kprobe_count/bpf_bpfeb.o
Wrote /home/caodailiang/go/src/github.com/cilium/ebpf/examples/kprobe_count/bpf_bpfeb.go
生成的文件:
$ ls kprobe_count/
bpf_bpfeb.go bpf_bpfeb.o bpf_bpfel.go bpf_bpfel.o kprobe.c
2. 用户态程序
用户态 main.go 文件,内容如下:
// This program demonstrates attaching an eBPF program to a kernel symbol.
// The eBPF program will be attached to the start of the sys_execve
// kernel function and prints out the number of times it has been called
// every second.
package main
import (
"log"
"time"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
const mapKey uint32 = 0
func main() {
// Name of the kernel function to trace.
fn := "sys_execve"
// Allow the current process to lock memory for eBPF resources.
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
// Load pre-compiled programs and maps into the kernel.
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// Open a Kprobe at the entry point of the kernel function and attach the
// pre-compiled program. Each time the kernel function enters, the program
// will increment the execution counter by 1. The read loop below polls this
// map value once per second.
kp, err := link.Kprobe(fn, objs.KprobeExecve, nil)
if err != nil {
log.Fatalf("opening kprobe: %s", err)
}
defer kp.Close()
// Read loop reporting the total amount of times the kernel
// function was entered, once per second.
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
log.Println("Waiting for events..")
for range ticker.C {
var value uint64
if err := objs.KprobeMap.Lookup(mapKey, &value); err != nil {
log.Fatalf("reading map: %v", err)
}
log.Printf("%s called %d times\n", fn, value)
}
}
代码已有详细注释,先通过 loadBpfObjects
将内核态bpf程序和map加载到内核中,然后通过 kprobe
将内核程序挂载到 sys_execve
函数,后续通过循环读取 map
中的数据进行输出。
编译运行该程序
$ go run -exec sudo main.go bpf_bpfel.go
2022/11/23 12:22:23 Waiting for events..
2022/11/23 12:22:24 sys_execve called 23 times
2022/11/23 12:22:25 sys_execve called 46 times
2022/11/23 12:22:26 sys_execve called 69 times
2022/11/23 12:22:27 sys_execve called 92 times
2022/11/23 12:22:28 sys_execve called 115 times
2022/11/23 12:22:29 sys_execve called 138 times
...
3. 使用go generate来驱动bpf2go转换
上述第1步中的 bpf2go
可由Go语言工具链提供的 go generate
工具来驱动,在 main.go 的 main 函数上面增加一行 go:generate 指示语句,语句内容即为之前的 bpf2go 命令。当我们执行 go generate 命令时,go generate 会扫描到该指示语句,并执行后面的命令。
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang-14 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf kprobe.c -- -I../headers
const mapKey uint32 = 0
func main() {
...
go generate
指示语句可以使用变量参数,变量的值在 Makefile 中定义。