Linux BPF技术深度解析(1):Hello World实现

一、介绍

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声明伪目标(cleanbpfloadbuild),避免与同名文件冲突。
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"))。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坏一点

您的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值