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

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. 执行并验证即可

http://www.xdnf.cn/news/7284.html

相关文章:

  • 使用 Flask 框架实现FTP,允许用户通过 Web 界面浏览和下载文件夹中的所有文件
  • Lombok
  • Docker 核心原理详解:Namespaces 与 Cgroups 如何实现资源隔离与限制
  • Better Faster Large Language Models via Multi-token Prediction 原理
  • Linux多进程 写时拷贝 物理地址和逻辑地址
  • 在嵌入式系统中, 一般链路层断开多久,断开TCP为好
  • GitHub排名第一的开源ERP项目:Odoo生产计划与执行的功能概述
  • 安装Anaconda后无jupyter解决方法
  • 【NLP】35. 构建高质量标注数据
  • HTTP 协议基础
  • DAY27
  • 【C语言基础语法入门】通过简单实例快速掌握C语言核心概念
  • Golang的Web应用架构设计
  • Python爬虫实战:获取国家统计网最新消费数据并分析,为从业者做参考
  • Profinet转Ethernet IP主站网关:点燃氢醌生产线的智慧之光!
  • 【技术追踪】心脏生理学知识驱动的扩散模型用于无对比剂心肌梗死增强(MICCAI-2024)
  • 云原生安全:错误策略S3存储桶ACL设置为Everyone:FullControl
  • 智能投影仪行业2025数据分析报告
  • 【RAG 系统高效召回1】评估指标
  • 每日Prompt:自拍生成摇头娃娃
  • 【Unity】Unity中将字典序列化
  • 为什么上传大量大文件推荐是使用 app 应用为不是 web 浏览器下载上传呢?
  • Java合并两个列表到目标列表,并且进行排序
  • 解决使用@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss“, timezone = “GMT+8“)时区转换无效的问题
  • leetcode3371. 识别数组中的最大异常值-medium
  • 软件架构之-论高并发下的可用性技术
  • 团队氛围紧张,如何提升工作积极性?
  • 交叉引用、多个参考文献插入、跨文献插入word/wps中之【插入[1-3]、连续文献】
  • 多类别异常检测新SOTA-MVMCAD
  • 中国城市间交通驾车距离矩阵(2024)