准备工作
环境配置
安装clang/llvm
centos:
1
| sudo yum install clang llvm
|
ubuntu:
1 2
| sudo apt update sudo apt install clang llvm
|
验证:
安装llvm-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
|
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_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程序的操作。
- 构建用户空间程序
- 最终链接
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_SRC := $(abspath ../libbpf/src)
BPFTOOL_SRC := $(abspath ../bpftool/src)
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
LIBBPF_OUTPUT := $(abspath $(OUTPUT)/libbpf)
BPFTOOL_OUTPUT := $(abspath $(OUTPUT)/bpftool)
BPFTOOL := $(BPFTOOL_OUTPUT)/bootstrap/bpftool
VMLINUX := ../vmlinux.h
INCLUDES := -I$(OUTPUT) -I../libbpf/include/uapi -I$(dir $(VMLINUX))
CFLAGS := -g -Wall
ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
APPS = hello
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_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): | $(BPFTOOL_OUTPUT) $(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap
$(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,$@)
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.ebpf.o | $(OUTPUT) $(BPFTOOL) $(BPFTOOL) gen skeleton $< > $@
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h $(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) $(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
$(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> #include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
int my_pid = 0;
SEC("tp/syscalls/sys_enter_write") int monitor_write_enter(void* ctx) { int pid = bpf_get_current_pid_tgid() >> 32;
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"
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_set_print(libbpf_print_fn);
skel = hello_ebpf__open(); if (!skel) { fprintf(stderr, "无法打开eBPF程序\n"); return 1; }
skel->bss->my_pid = getpid();
err = hello_ebpf__load(skel); if (err) { fprintf(stderr, "加载和验证eBPF程序失败\n"); goto cleanup; }
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 (;;) { fprintf(stderr, "."); sleep(1); }
cleanup: hello_ebpf__destroy(skel); return -err; }
|
4. 执行并验证即可
kprobe
简介
概述:
是一种动态内核探测技术,允许开发者在内核函数的任意指令位置插入探测点,实时捕获函数调用、参数、返回值及执行上下文。并非是ebpf独有的,传统上可以通过编写一个自定义内核模块,以便从kprobe调用,ebpf简化了这个过程。
类型:
ebpf和kprobe的结合:
- bpf程序加载:开发者编写ebpf程序,通过
SEC("kprobe/function_name")
声明探测点,编译为bpf字节码后加载到内核
- 数据交互:bpf程序通过
bpf_printk
输出调试信息,或通过maps将数据传递到用户态程序进行聚合分析
工作机制
1. 注册kprobe:
当用户通过register_kprobe()
注册一个探测点时,kprobes会做两件事:
2. CPU命中断点指令后的处理:
- 触发trap:引发cpu硬件异常,进入内核的异常处理流程
- 保存寄存器:cpu自动将当前寄存器状态(如程序计数器、通用寄存器等)保存到内核栈中,形成
pt_regs
结构体
- 通过
notifier_call_chain
传递控制权:这是liunx
kernel的一种通知链机制。kprobes会注册一个回调函数到该链表中,当异常发生时,内核通过该链表通知kprobes处理程序
- 执行
pre_handler
:用户自定义的预处理函数,能通过pt_regs
访问寄存器状态
3. 单步执行探测指令副本:
在pre_handler
完成后,需要执行被探测的原始指令,但是为了避免竞态条件:
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>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC("kprobe/do_unlinkat") int BPF_KPROBE(do_unlinkat, int dfd, struct filename* name) { pid_t pid; const char* filename;
pid = bpf_get_current_pid_tgid() >> 32; filename = BPF_CORE_READ(name, name); bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename); return 0; }
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"
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_set_print(libbpf_print_fn);
skel = kprobe_ebpf__open_and_load(); if (!skel) { fprintf(stderr, "打开和加载eBPF程序失败\n"); return 1; }
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. 执行并验证即可