一、介绍
BPF(Berkeley Packet Filter伯克利数据包过滤器)是Linux内核中的一项高效技术,最初用于网络数据包过滤,现已扩展至系统性能分析、安全监控等领域。本质上是一个运行在内核态的虚拟机,最初用于优化 tcpdump 包过滤效率,其工作原理主要涉及程序加载、数据包过滤和数据交互等方面。
1.1 工作原理
(1)BPF 程序加载
- 首先编写 BPF 的 C 语言程序,此程序只能使用内核提供的
BPF_XXX
帮助函数。 - 通过 LLVM、Clang 等编译器,将 C 程序编译为 BPF 字节码,这是一种类似于汇编语言的程序,编译好的字节码存放于 ELF 文件中的段中。
- 使用系统调用
bpf()
将字节码注入到内核的入口(Ingress)。注入前,内核会使用 “检查器” 对字节码的合法性、规范性进行检查,通过检查的字节码才会被附着到内核中。 - 最后使用 JIT 编译器将字节码编译成可在本机 CPU 上运行的指令。
(2)数据包过滤
- Linux 内核维护者会开发一些 hook 点,BPF 程序可以挂载在这些 hook 点上。当 hook 点对应的事件发生时,如数据包到达网络接口,就会执行挂载的 BPF 程序。BPF 程序可以从数据包中获取数据,对数据进行算术运算,并将结果与常量或数据包中的其他数据进行比较,根据比较结果决定接受还是拒绝该数据包。例如,XDP 类型的 BPF 程序挂载在指定网络接口上,若数据包不符合规则,BPF 程序返回
XDP_DROP
,内核就会丢弃该数据包。
(3)数据交互
- BPF 程序和用户空间程序通过 BPF 映射通信。BPF 映射以键 / 值对形式保存在内核,可被任何 BPF 程序访问,用户空间程序则可通过文件描述符访问 BPF 映射。例如,用户态程序可以修改映射中 BPF 程序访问的配置数据,如黑名单规定的 IP 列表和域名,来改变 BPF 程序的运行方式;BPF 程序也可以将统计的数据包信息保存到 BPF 映射,供用户态程序获取。
1.2 BPF的核心优势
- 高性能:BPF程序在内核空间运行,避免了用户态与内核态的频繁切换。
- 安全性:通过验证器确保程序不会导致内核崩溃或资源泄漏。
- 灵活性:支持动态加载和卸载,无需重启系统。
二、环境搭建
2.1 主机选择
选择在windows的虚拟机里面安装ubuntu系统,通过”uname -r“获得内核版本,本教程使用ubuntu20.04。
输入:uname -r
返回:5.15.0-139-generic
2.3 部署内核源码环境
在内核源码下载网页选择kernel5.15.0版本对应的内核。
#第一步:安装一些构建依赖项和示例所需的所有工具
$ sudo apt update
$ sudo apt install build-essential git make libelf-dev clang strace tar bpfcc-tools linux-headers-$(uname -r) gcc-multilib
第二步:获取当前内核的源代码版本
$ cd /tmp
$ git clone --depth 1 git://kernel.ubuntu.com/ubuntu/ubuntu-bionic.git
#若上述命令执行超时,可能git://协议被屏蔽,改用如下方法获取
$ git clone --depth 1 https://kernel.ubuntu.com/git/ubuntu/ubuntu-bionic.git
$ sudo mv ubuntu-bionic /kernel-src
$ cd /kernel-src/tools/lib/bpf
$ sudo make && sudo make install prefix=/usr/local
$ sudo mv /usr/local/lib64/libbpf.* /lib/x86_64-linux-gnu/
三、第一个demo
3.1 文件目录
hello_world# tree
.
├── bpf_program.c
├── bpf_program.o
├── loader.c
├── Makefile
├── monitor-exec
└── README.md
3.2 bpf_program.c文件
#include <linux/bpf.h>
#define SEC(NAME) __attribute__((section(NAME), used))
static int (*bpf_trace_printk)(const char *fmt, int fmt_size,
...) = (void *)BPF_FUNC_trace_printk;
SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
char msg[] = "Hello, BPF World!";
bpf_trace_printk(msg, sizeof(msg));
return 0;
}
char _license[] SEC("license") = "GPL";
代码段 | 功能说明 | 关键点 |
---|---|---|
头文件引入 | #include <linux/bpf.h> | 引入 Linux 内核提供的 eBPF 头文件,定义了 eBPF 相关宏、数据结构和函数(如 BPF_FUNC_trace_printk )。 |
宏定义 | #define SEC(NAME) __attribute__((section(NAME), used)) | 定义宏 SEC(NAME) ,用于指定函数或变量存储在 ELF 文件的特定段(Section)中,used 避免编译器优化丢弃符号。 |
函数指针定义 | static int (*bpf_trace_printk)(const char *fmt, int fmt_size, ...) = (void *)BPF_FUNC_trace_printk; | 声明一个函数指针 bpf_trace_printk ,指向内核辅助函数 BPF_FUNC_trace_printk (用于打印调试信息到跟踪管道)。 |
eBPF 程序入口 | SEC("tracepoint/syscalls/sys_enter_execve") int bpf_prog(void *ctx) | 定义 eBPF 程序入口函数 bpf_prog ,挂载到 tracepoint/syscalls/sys_enter_execve 跟踪点(即 execve 系统调用进入时触发)。- ctx :上下文参数(此处未使用)。 |
程序逻辑 | char msg[] = "Hello, BPF World!"; bpf_trace_printk(msg, sizeof(msg)); | 定义字符串 msg ,并通过 bpf_trace_printk 将其打印到内核跟踪管道(可通过 /sys/kernel/debug/tracing/trace_pipe 读取)。 |
返回值 | return 0; | 返回 0 表示允许系统调用继续执行(eBPF 程序常用返回值,具体语义取决于挂载点类型)。 |
许可证声明 | char _license[] SEC("license") = "GPL"; | 声明 eBPF 程序的许可证为 GPL (某些内核功能要求程序必须是 GPL 兼容许可证)。 |
3.3 loader.c文件
#include "bpf_load.h"
#include <stdio.h>
int main(int argc, char **argv) {
if (load_bpf_file("bpf_program.o") != 0) {
printf("The kernel didn't load the BPF program\n");
return -1;
}
read_trace_pipe();
return 0;
}
代码段 | 功能说明 | 关键点 |
---|---|---|
头文件引入 | #include "bpf_load.h" | 引入自定义头文件 bpf_load.h ,提供加载 eBPF 程序(bpf_program.o )和读取跟踪数据的函数(如 load_bpf_file() 和 read_trace_pipe() )。 |
#include <stdio.h> | 引入标准输入输出库,用于打印错误信息(如 printf )。 | |
main 函数 | int main(int argc, char **argv) | 程序入口,接收命令行参数(虽未使用)。 |
加载 eBPF 程序 | if (load_bpf_file("bpf_program.o") != 0) | 调用 load_bpf_file() 加载编译好的 eBPF 对象文件(bpf_program.o )。 |
printf("The kernel didn't load the BPF program\n"); | 如果加载失败(返回非零值),打印错误信息并返回 -1 。 | |
return -1; | 异常退出,表示程序初始化失败。 | |
读取跟踪数据 | read_trace_pipe(); | 调用 read_trace_pipe() 从内核的跟踪管道(/sys/kernel/debug/tracing/trace_pipe )读取 eBPF 程序的输出数据。 |
正常退出 | return 0; | 程序成功执行后退出。 |
3.4 makefile文件
CLANG = clang
EXECABLE = monitor-exec
BPFCODE = bpf_program
BPFTOOLS = /kernel-src/samples/bpf
BPFLOADER = $(BPFTOOLS)/bpf_load.c
CCINCLUDE += -I/kernel-src/tools/testing/selftests/bpf
LOADINCLUDE += -I/kernel-src/samples/bpf
LOADINCLUDE += -I/kernel-src/tools/lib
LOADINCLUDE += -I/kernel-src/tools/perf
LOADINCLUDE += -I/kernel-src/tools/include
LIBRARY_PATH = -L/usr/local/lib64
BPFSO = -lbpf
CFLAGS += $(shell grep -q "define HAVE_ATTR_TEST 1" /kernel-src/tools/perf/perf-sys.h \
&& echo "-DHAVE_ATTR_TEST=0")
.PHONY: clean $(CLANG) bpfload build
clean:
rm -f *.o *.so $(EXECABLE)
build: ${BPFCODE.c} ${BPFLOADER}
$(CLANG) -O2 -target bpf -c $(BPFCODE:=.c) $(CCINCLUDE) -o ${BPFCODE:=.o}
bpfload: build
clang $(CFLAGS) -o $(EXECABLE) -lelf $(LOADINCLUDE) $(LIBRARY_PATH) $(BPFSO) \
$(BPFLOADER) loader.c
$(EXECABLE): bpfload
.DEFAULT_GOAL := $(EXECABLE)
部分 | 内容 | 解释 |
---|---|---|
变量定义 | CLANG = clang | 指定使用 clang 编译器编译 eBPF 程序和加载器。 |
目标规则 | EXECABLE = monitor-exec | 定义最终生成的可执行文件名称(monitor-exec )。 |
BPFCODE = bpf_program | 定义 eBPF 源文件(bpf_program.c )的基名(不带扩展名)。 | |
BPFTOOLS = /kernel-src/samples/bpf | 指定内核源码中 BPF 示例工具的路径(用于加载器)。 | |
BPFLOADER = $(BPFTOOLS)/bpf_load.c | 指定 BPF 加载器源文件(bpf_load.c )的路径。 | |
CCINCLUDE += -I/kernel-src/tools/testing/selftests/bpf | 编译 eBPF 程序时添加的头文件搜索路径(CCINCLUDE )。 | |
LOADINCLUDE += -I/kernel-src/samples/bpf LOADINCLUDE += -I/kernel-src/tools/lib LOADINCLUDE += -I/kernel-src/tools/perf LOADINCLUDE += -I/kernel-src/tools/include | 编译加载器程序时添加的头文件搜索路径(LOADINCLUDE )。 | |
LIBRARY_PATH = -L/usr/local/lib64 | 指定库文件搜索路径(-L/usr/local/lib64 )。 | |
BPFSO = -lbpf | 指定链接的库(libbpf )。 | |
CFLAGS += $(shell grep -q "define HAVE_ATTR_TEST 1" /kernel-src/tools/perf/perf-sys.h && echo "-DHAVE_ATTR_TEST=0") | 动态检查内核源码中的 perf-sys.h ,如果定义了 HAVE_ATTR_TEST ,则添加 -DHAVE_ATTR_TEST=0 (用于兼容某些内核版本)。 | |
.PHONY: clean $(CLANG) bpfload build | 声明伪目标(clean 、bpfload 、build ),避免与同名文件冲突。 | |
clean: rm -f *.o *.so $(EXECABLE) | 删除生成的中间文件(.o 、.so )和最终可执行文件(monitor-exec )。 | |
build: ${BPFCODE.c} ${BPFLOADER} $(CLANG) -O2 -target bpf -c $(BPFCODE:=.c) $(CCINCLUDE) -o ${BPFCODE:=.o} | 编译 eBPF 程序(bpf_program.c ):- -O2 :优化级别。- -target bpf :生成 eBPF 字节码。- -c :只编译不链接。- $(CCINCLUDE) :添加头文件搜索路径。- 输出: bpf_program.o 。 | |
bpfload: build clang $(CFLAGS) -o $(EXECABLE) -lelf $(LOADINCLUDE) $(LIBRARY_PATH) $(BPFSO) $(BPFLOADER) loader.c | 编译加载器程序(bpf_load.c 和 loader.c ):- $(CFLAGS) :动态生成的编译选项。- -lelf :链接 libelf 库。- $(LOADINCLUDE) :添加头文件搜索路径。- $(LIBRARY_PATH) :添加库文件搜索路径。- $(BPFSO) :链接 libbpf 库。- 输出: monitor-exec 。 | |
$(EXECABLE): bpfload | 声明 monitor-exec 依赖于 bpfload ,确保加载器先编译。 | |
.DEFAULT_GOAL := $(EXECABLE) | 设置默认目标为 monitor-exec (直接运行 make 时生成该文件)。 |
3.5 运行结果
四、demo的功能和实际用途
4.1 功能
(1)监控 execve
系统调用
- 当任何进程调用
execve
(执行新程序)时,触发 eBPF 程序。 - 示例中未使用
ctx
参数,但实际可通过它获取进程信息(如 PID、命令行参数等)。
(2)打印调试信息
- 每次触发时,通过
bpf_trace_printk()
向内核跟踪缓冲区输出固定字符串:Hello, BPF World!
- 用户可通过
cat /sys/kernel/debug/tracing/trace_pipe
实时查看这些消息。
(3)许可证声明
- 明确程序为
GPL
许可证,确保内核允许加载(因调用了bpf_trace_printk
等 GPL 受限函数)。
4.2 用途
(1)调试与学习
- 验证 eBPF 程序能否正确挂载到系统调用跟踪点。
- 确认
bpf_trace_printk
的输出机制是否正常工作。
(2)基础监控模板
- 可扩展为监控特定进程的启动行为(例如检测恶意程序执行)。
- 通过修改
ctx
参数解析,可获取被执行程序的路径或参数(需结合bpf_probe_read
等函数)。
(3)教学示例
- 展示 eBPF 程序的基本结构:
- 如何定义挂载点(
SEC("tracepoint/...")
)。 - 如何调用内核辅助函数(
BPF_FUNC_trace_printk
)。 - 如何声明许可证(
SEC("license")
)。
- 如何定义挂载点(