当前位置: 首页 > news >正文

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_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程序的操作。
  • 构建用户空间程序
  • 最终链接
# 输出目录
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. 执行并验证即可

相关文章:

  • frida 配置
  • OCframework编译Swift
  • 【C++]string模拟实现
  • C++编程this指针练习
  • 【科研项目】大三保研人科研经历提升
  • Python元组全面解析:从入门到精通
  • 【基础】Windows开发设置入门8:Windows 子系统 (WSL)操作入门
  • 深入解析Java四大引用类型:从强引用到虚引用的内存管理艺术
  • 软件设计师E-R模型考点分析——求三连
  • STM32实战指南:DHT11温湿度传感器驱动开发与避坑指南
  • 关于ECMAScript的相关知识点!
  • 认识常规贴片电阻
  • 数学实验(方程和微分方程求解)
  • 11.4/Q1,GBD数据库最新文章解读
  • 第二十一次博客打卡
  • Prompt、Agent、MCP关系
  • Mergekit——高频合并算法 TIES解析
  • 嵌入式(C语言篇)Day10
  • DAPO:用于指令微调的直接偏好优化解读
  • 让数据驱动增长更简单! ClkLog用户行为分析系统正式入驻GitCode
  • 习近平:坚定信心推动高质量发展高效能治理,奋力谱写中原大地推进中国式现代化新篇章
  • 15年全免费,内蒙古准格尔旗实现幼儿园到高中0学费
  • 新任重庆市垫江县委副书记刘振已任县政府党组书记
  • 国家防汛抗旱总指挥部对15个重点省份开展汛前实地督导检查
  • 土耳其、美国、乌克兰三边会议开始
  • 日本一季度实际GDP环比下降0.2%