ebpf程序入门编写

准备工作

环境配置

安装clang/llvm

centos:

1
sudo yum install clang llvm

ubuntu:

1
2
sudo apt update
sudo apt install clang llvm

验证:

1
clang --version

安装llvm-bpf库

验证:

1
llvm-config --libs bpf

安装Zlib

centos:

1
sudo yum install zlib zlib-devel

ubuntu:

1
sudo apt install zlib1g zlib1g-dev

安装libelf

centos:

1
sudo yum install elfutils-libelf-devel

ubuntu:

1
sudo apt install libelf-dev

验证:

1
pkg-config --libs libelf

安装libbpf

centos:

1
sudo yum install libbpf-devel

ubuntu:

1
sudo apt install libbpf-dev

验证:

1
pkg-config --modversion libbpf

安装bpftool

centos:

1
sudo yum install bpftool

vmlinux.h

概述: ebpf程序需要直接访问内核数据结构的定义,而vmlinux.h是包含这些定义的权威头文件

如何获取: 通过bpftool工具

1
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_completeblock_rq_issue事件,分解存储设备的io延迟
  • 安全监控:
    • 敏感操作审计:通过mmapptrace事件的Tracepoint,检测非法内存访问或调试行为

性能优化建议:

  • 选择低开销挂钩方式:优先使用Raw Tracepoint 或 Fentry(基于Trampoline机制),相比普通Tracepoint减少30%-50%的指令开销
  • 减少数据复制:通过bpf_perf_event_output直接向用户态推送聚合数据,避免频繁读取缓冲区
  • 动态字段适配:使用BTFbpf_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程序的操作。
  • 构建用户空间程序
  • 最终链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 输出目录
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程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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位是TGID
int pid = bpf_get_current_pid_tgid() >> 32;

// 只处理我们关注的进程ID
if (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. 编写用户态程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#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; // 跳转到清理流程
}

// 挂载到tracepoint
err = 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的使用步骤

1. 编写ebpf程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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;

// 当前进程pid
pid = 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. 编写用户空间程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#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;
}

// 挂载到kprobe
err = 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. 执行并验证即可


ebpf程序入门编写
http://example.com/2025/05/19/ebpf程序入门编写/
作者
凌云行者
发布于
2025年5月19日
许可协议