ebpf程序入门编写
准备工作
环境配置
安装clang/llvm
centos:
sudo yum install clang llvm
ubuntu:
sudo apt update
sudo apt install clang llvm
验证:
clang --version
安装llvm-bpf库
验证:
llvm-config --libs bpf
安装Zlib
centos:
sudo yum install zlib zlib-devel
ubuntu:
sudo apt install zlib1g zlib1g-dev
安装libelf
centos:
sudo yum install elfutils-libelf-devel
ubuntu:
sudo apt install libelf-dev
验证:
pkg-config --libs libelf
安装libbpf
centos:
sudo yum install libbpf-devel
ubuntu:
sudo apt install libbpf-dev
验证:
pkg-config --modversion libbpf
安装bpftool
centos:
sudo yum install bpftool
vmlinux.h
概述: ebpf程序需要直接访问内核数据结构的定义,而vmlinux.h
是包含这些定义的权威头文件
如何获取: 通过bpftool工具
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
Tracing program type
仓库代码: https://github.com/1037827920/libbpf-template.git
tracepoint
简介
概述: 是一种内核静态预置的钩子机制,允许开发者在内核代码的特定位置插入探针,用于收集运行时信息。具有更低的开销和更高的稳定性
应用场景:
- 性能分析:
- 调度延迟统计:通过
sched_switch
事件记录进程切换时间,分析调度器性能瓶颈 - 系统调用追踪:挂载到
sys_enter_execve
等事件,监控进程启动行为
- 调度延迟统计:通过
- 跨线程问题诊断:
- 锁竞争分析:在锁获取/释放的Tracepoint上附加ebpf程序,统计等待时间和持有线程等调用栈
- io延迟分析:结合
block_rq_complete
和block_rq_issue
事件,分解存储设备的io延迟
- 安全监控:
- 敏感操作审计:通过
mmap
或ptrace
事件的Tracepoint,检测非法内存访问或调试行为
- 敏感操作审计:通过
性能优化建议:
- 选择低开销挂钩方式:优先使用Raw Tracepoint 或 Fentry(基于Trampoline机制),相比普通Tracepoint减少30%-50%的指令开销
- 减少数据复制:通过
bpf_perf_event_output
直接向用户态推送聚合数据,避免频繁读取缓冲区 - 动态字段适配:使用
BTF
和bpf_core_read
宏处理不同内核版本的结构体字段片一差异
核心实现
内核源码结构:
Tracpoint在内核代码中通过宏定义实现,例如调度类Tracepoint的定义位于/include/trace/events/sched.h
中,通过TRACE_EVENT
宏声明事件参数和数据结构。关键源码文件包括:
include/linux/tracepoint-defs.h
:定义strcut tracepoint
,包含名称、注册函数指针、静态调用等核心字段include/linux/tracepoint.h
:提供注册/注销api(如tracepoint_probe_register
)和关键宏(如__DO_TRACE
执行探针逻辑)
数据传递流程:
- 当Tracepoint被触发时,内核会将参数写入perf环形缓冲区,并将缓冲区传递给ebpf程序
- ebpf程序通过
bpf_probe_read
系列辅助函数安全读取缓冲区中的数据,例如解析sched_switch
事件中的进程名和PID
使用步骤
相关代码仓库:
1. 编写Makefile: 主要是用来编译libbpf、bpftool,然后编译ebpf程序,利用bpftool自动创建用户态与内核态之间的接口,封装bpt丢像加载、映射管理、事件处理等底层操作
主要操作:
- 创建必要的目录
- 构建libbpf静态库
- 构建bpftool工具
- 构建ebpf程序
- 生成.skel.h头文件,利用bpttool gen skeleton自动创建用户态与内核态之间的交互接口,封装了打开、加载、挂载、销毁ebpf程序的操作。
- 构建用户空间程序
- 最终链接
# 输出目录
OUTPUT := .output
# 编译器
CLANG := clang
# libbpf源码路径
LIBBPF_SRC := $(abspath ../libbpf/src)
# bpftool源码路径
BPFTOOL_SRC := $(abspath ../bpftool/src)
# 静态库路径
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
# libbpf输出目录
LIBBPF_OUTPUT := $(abspath $(OUTPUT)/libbpf)
# bpftool输出目录
BPFTOOL_OUTPUT := $(abspath $(OUTPUT)/bpftool)
# bpftool二进制文件
BPFTOOL := $(BPFTOOL_OUTPUT)/bootstrap/bpftool
# 内核头文件路径
VMLINUX := ../vmlinux.h
# 头文件包含路径
INCLUDES := -I$(OUTPUT) -I../libbpf/include/uapi -I$(dir $(VMLINUX))
# 编译选项:-g 生成调试信息 -Wall 启用所有编译警告
CFLAGS := -g -Wall
# 链接选项:加上系统中环境变量要求的链接选项
ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
# 程序名
APPS = hello# 自定义的makefile宏,用于安全地设置变量值
# 只有当变量未被环境变量或命令行参数设置时,才赋予默认值
define allow-override$(if $(or $(findstring environment,$(origin $(1))),\$(findstring command line,$(origin $(1)))),,\$(eval $(1) = $(2)))
endef$(call allow-override,CC,$(CROSS_COMPILE)cc)
$(call allow-override,LD,$(CROSS_COMPILE)ld).PHONY: all
all: $(APPS).PHONY: clean
clean:rm -rf $(OUTPUT) $(APPS)# 目录创建
$(OUTPUT) $(LIBBPF_OUTPUT) $(BPFTOOL_OUTPUT):mkdir -p $@# 构建libbpf静态库
# 使用wildcard匹配所有.c.h文件和Makefile文件,并通过|确保LIBBPF_OUTPUT存在
# 1. $(MAKE) -C $(LIBBPF_SRC):进入libbpf源码目录,执行make命令,构建libbpf静态库
# 2. BUILD_STATIC_ONLY=1:只构建静态库
# 3. OBJDIR=$(dir $@)/libbpf:指定libbpf静态库的输出目录,$(dir $@)表示目标文件的目录(.output/)
# 4. DESTDIR=$(dir $@):指定libbpf静态库的安装目录,$(dir $@)表示目标文件的目录(.output/)
# 5. install:执行libbpf源码目录的Makefile中的install目标,将libbpf静态库安装到指定目录
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(LIBBPF_OUTPUT)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \INCLUDEDIR= LIBDIR= UAPIDIR= \install# 构建bpftool工具
# 1. 进入bpftool源码目录,执行make命令,构建bpftool工具
# 2. ARCH=:指定构建目标架构,这里为空,表示构建当前主机架构
# 3. CROSS_COMPILE=:指定交叉编译工具链前缀,这里为空,表示使用当前主机工具链
# 4. OUTPUT=$(BPFTOOL_OUTPUT)/:指定bpftool工具的输出目录
# 5. bootstrap:执行bpftool源码目录的Makefile中的bootstrap目标,构建bpftool工具
$(BPFTOOL): | $(BPFTOOL_OUTPUT)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap# 构建ebpf程序
# 1. .ebpf.c -> .ebpf.o
# 第一条命令:编译ebpf程序
# 第二条命令:利用bpftool生成最终的bpf对象文件
# filter只过滤出.c文件进行编译
# patsubst将.ebpf.c替换为.tmp.ebpf.o,返回为$@,即目标文件
$(OUTPUT)/%.ebpf.o: %.ebpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_x86 \$(INCLUDES) \-c $(filter %.c,$^) -o $(patsubst %.ebpf.o,%.tmp.ebpf.o,$@)$(BPFTOOL) gen object $@ $(patsubst %.ebpf.o,%.tmp.ebpf.o,$@)# 2. 生成骨架头文件,该文件包含bpf程序的所有元信息
# 使用bpftool自动创建了用户态与内核态之间的交互接口,封装了bpf对象加载、映射管理、事件处理等底层操作
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.ebpf.o | $(OUTPUT) $(BPFTOOL)$(BPFTOOL) gen skeleton $< > $@# 3. 编译用户空间程序
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@# 4. 最终链接
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@# 出错时删除不完整目标
.DELETE_ON_ERROR:# 保留中间文件
.SECONDARY:
2. 编写ebpf程序:
#include <linux/bpf.h> // 要在bpf_helpers.h之前包含,不然就会报错,破案了,是因为自动保存修改了头文件的顺序,所以我之前才一直编译不成功,现在我已经取消头文件排序了
#include <bpf/bpf_helpers.h>// 声明BSD/GPL许可证
char LICENSE[] SEC("license") = "Dual BSD/GPL";// 存储当前进程PID的全局变量
// 用户程序会在加载ebpf程序前修改这个值
int my_pid = 0;// SEC宏定义eBPF程序的挂载点,这里挂载到进入write系统调用的跟踪点(tracepoint)
SEC("tp/syscalls/sys_enter_write")
int monitor_write_enter(void* ctx) {// 获取当前触发事件的进程ID// bpf_get_current_pid_tgid()返回64位值,高32位是PID,低32位是TGIDint pid = bpf_get_current_pid_tgid() >> 32;// 只处理我们关注的进程IDif (pid != my_pid)return 0;// 在内核日志中打印信息bpf_printk("Hello ebpf from PID %d.\n", pid);return 0;
}SEC("tp/syscalls/sys_exit_write")
int monitor_write_exit(void* ctx) {int pid = bpf_get_current_pid_tgid() >> 32;if (pid != my_pid)return 0;bpf_printk("Goodbye ebpf from PID %d.\n", pid);return 0;
}
3. 编写用户态程序:
#include <bpf/libbpf.h>
#include <stdio.h>
#include <sys/resource.h>
#include <unistd.h>
#include "hello.skel.h"// libbpf日志回调函数
static int libbpf_print_fn(enum libbpf_print_level level,const char* format,va_list args) {return vfprintf(stderr, format, args); // 将日志输出到标准错误
}int main(int argc, char** argv) {struct hello_ebpf* skel;int err;// 设置libbpf的错误和调试信息回调函数libbpf_set_print(libbpf_print_fn);// 打开eBPF应用程序skel = hello_ebpf__open();if (!skel) {fprintf(stderr, "无法打开eBPF程序\n");return 1;}skel->bss->my_pid = getpid(); // 获取当前进程PID// 加载并验证eBPF程序err = hello_ebpf__load(skel);if (err) {fprintf(stderr, "加载和验证eBPF程序失败\n");goto cleanup; // 跳转到清理流程}// 挂载到tracepointerr = hello_ebpf__attach(skel);if (err) {fprintf(stderr, "附加eBPF程序失败\n");goto cleanup;}printf("成功启动! 请运行 `sudo cat ""/sys/kernel/debug/tracing/trace_pipe` ""查看BPF程序的输出.\n");// 主循环 - 保持程序运行for (;;) {// 触发BPF程序执行fprintf(stderr, "."); // 会调用write系统调用sleep(1); // 每秒输出一个点}cleanup:// 清理资源hello_ebpf__destroy(skel);return -err;
}
4. 执行并验证即可
kprobe
简介
概述: 是一种动态内核探测技术,允许开发者在内核函数的任意指令位置插入探测点,实时捕获函数调用、参数、返回值及执行上下文。并非是ebpf独有的,传统上可以通过编写一个自定义内核模块,以便从kprobe调用,ebpf简化了这个过程。
类型:
- kprobe
- kretprobe
ebpf和kprobe的结合:
- bpf程序加载:开发者编写ebpf程序,通过
SEC("kprobe/function_name")
声明探测点,编译为bpf字节码后加载到内核 - 数据交互:bpf程序通过
bpf_printk
输出调试信息,或通过maps将数据传递到用户态程序进行聚合分析
工作机制
1. 注册kprobe: 当用户通过register_kprobe()
注册一个探测点时,kprobes会做两件事:
-
复制探测指令:将别探测位置的原始指令(比如说函数入口的代码)复制一份副本,用于后续恢复执行
-
插入断点指令:将探测点的第一条指令替换(如x86的
in3
)。效果:通过断电中断正常执行流,将控制权交给kprobes的回调函数,同时保留原始指令以便恢复
2. CPU命中断点指令后的处理:
- 触发trap:引发cpu硬件异常,进入内核的异常处理流程
- 保存寄存器:cpu自动将当前寄存器状态(如程序计数器、通用寄存器等)保存到内核栈中,形成
pt_regs
结构体 - 通过
notifier_call_chain
传递控制权:这是liunx kernel的一种通知链机制。kprobes会注册一个回调函数到该链表中,当异常发生时,内核通过该链表通知kprobes处理程序 - 执行
pre_handler
:用户自定义的预处理函数,能通过pt_regs
访问寄存器状态
3. 单步执行探测指令副本: 在pre_handler
完成后,需要执行被探测的原始指令,但是为了避免竞态条件:
-
移除断点指令:临时恢复原始指令,以便正确执行
-
单步执行副本:cpu进入单步调试模式(每条指令完成后都会触发异常),逐步执行复制的指令副本。执行完成后,会再次出发异常,通知kprobes继续处理
为什么用副本?
直接执行原指令会导致短暂的事件窗口(移除断点指令期间),其他CPU可能绕过探测点,导致数据竞争或逻辑错误
4. 执行post_handler
:
- 执行
post_handler
:用户自定义的后处理函数 - 恢复执行六:kprobes恢复断点指令,cpu继续执行探测点之后的代码
使用步骤
相关的代码仓库:
同样需要编写Makefile文件,具体看[tracepoint的使用步骤](# tracepoint)
1. 编写ebpf程序:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>// 声明BSD/GPL许可证
char LICENSE[] SEC("license") = "Dual BSD/GPL";// 声明监控内核函数do_unlinkat的入口
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(do_unlinkat,int dfd,struct filename* name) { // 自动获取内核参数pid_t pid;const char* filename;// 当前进程pidpid = bpf_get_current_pid_tgid() >> 32;// 通过bpf_core_read宏安全读取内核结构体中的文件名filename = BPF_CORE_READ(name, name);bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename);return 0;
}// 声明监控内核函数do_unlinkat的退出
SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(do_unlinkat_exit, long ret) {pid_t pid;pid = bpf_get_current_pid_tgid() >> 32;bpf_printk("KPROBE EXIT: pid = %d, ret = %ld\n", pid, ret);return 0;
}
2. 编写用户空间程序:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "kprobe.skel.h"// libbpf日志回调函数
static int libbpf_print_fn(enum libbpf_print_level level,const char* format,va_list args) {return vfprintf(stderr, format, args);
}int main(int argc, char** argv) {struct kprobe_ebpf* skel;int err;// 设置libbpf的错误和调试信息回调函数libbpf_set_print(libbpf_print_fn);// 打开并加载验证eBPF应用程序skel = kprobe_ebpf__open_and_load();if (!skel) {fprintf(stderr, "打开和加载eBPF程序失败\n");return 1;}// 挂载到kprobeerr = kprobe_ebpf__attach(skel);if (err) {fprintf(stderr, "附加eBPF程序失败\n");goto cleanup;}printf("成功启动! 请运行 `sudo cat ""/sys/kernel/debug/tracing/trace_pipe` ""查看BPF程序的输出.\n");// 主循环 - 保持程序运行for (;;) {fprintf(stderr, ".");sleep(1);}cleanup:kprobe_ebpf__destroy(skel);return -err;
}
3. 执行并验证即可