eBPF编程指南(一):eBPF初体验
1 什么是EBPF?
EBPF是一种可以让程序员在内核态执行自己的程序的机制,但是,为了安全起见,无法像内核模块一样随意调用内核的函数,只能调用一些bpf提前定义好的函数。为了让内核执行程序员自己的代码,需要指定HOOK点,相当于当内核执行到某个地方时就会执行程序员的代码,这有点类似于ftrace。
EBPF程序包含两个部分:
- 内核态代码:编译为.o文件,在内核态执行,将数据发送到队列
- 用户态代码:将内核态代码的.o文件加载到内核,接收队列中的数据并分析
2 Hello World(BCC)
EBPF有多种开发方式,按照语言来分的话,有两种方式:一种是内核态代码和用户态代码都用C语言开发;另一种是内核态代码用C语言开发,而用户态代码可以其他语言开发,例如,Python、Golang、Lua等。
BCC是一个方便构建EBPF程序的工具,让编写EBPF程序更加简单,内核态代码依旧采用C语言,而用户态代码可以采用Python和Lua。
下面就是一个简单的Hello world版本的EBPF程序,该程序对execve系统调用进行HOOK,内核态程序只执行一行打印语句,用户态程序负责将数据打印出来,因此,每当系统中创建一个进程,该程序就会输出一行Hello, BPF World!。
在执行之前,需要根据操作系统安装一些包:Installing BCC。
from bcc import BPFprogram = """
#include <uapi/linux/ptrace.h>int syscall__execve(struct pt_regs *ctx,const char __user *filename,const char __user *const __user *__argv,const char __user *const __user *__envp)
{char msg[] = "Hello, BPF World!";bpf_trace_printk("%s", msg);return 0;
}
"""b = BPF(text=program)
execve_fnname = b.get_syscall_fnname("execve")
b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve")
b.trace_print()
可以看到,使用BCC开发EBPF程序还是比较简单的,用户态程序只需要定义好要HOOK的点,并接收数据,主要的逻辑其实还是在内核态代码中。
但是,这种方式不便于理解EBPF的工作方式,比如,program这段程序代码是怎么载入到内核的呢?载入之前需要编译吗?那Python里面是怎么编译的呢?
3 EBPF的基于C语言的开发模式
要理解EBPF的工作方式,最好的方式还是基于纯C语言开发EBPF程序。
如果用C语言开发,内核态代码和用户态代码就必须分开写,内核态代码就需要知道可以调用哪些函数,而用户态代码就需要知道如何载入内核态编译后的二进制。
现在常用的开发方式有两种,一种是基于原生libbpf库开发,另一种是基于libbpf-bootstrap开发。
4 基于原生libbpf库
首先是环境的搭建:
- 安装依赖:yum install clang elfutils-libelf-devel,其中clang用于编译内核态代码,elfutils-libelf-devel用于处理内核态代码编译成的二进制
- 安装libbpf:git clone https://github.com/libbpf/libbpf && cd libbpf/src && make && make install
- 生成vmlinux.h,解除对kernel header的依赖:bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
内核态代码和用户态代码都需要引入的头文件:
#ifndef __EXECVE_H
#define __EXECVE_Hstruct event {pid_t pid;pid_t ppid;
};#endif
内核态代码:
#include "vmlinux.h"
#include "execve.h"
#include <bpf/bpf_helpers.h>static const struct event empty_event = {};struct {__uint(type, BPF_MAP_TYPE_HASH);__uint(max_entries, 1024);__type(key, pid_t);__type(value, struct event);
} execs SEC(".maps");struct {__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);__uint(key_size, sizeof(u32));__uint(value_size, sizeof(u32));
} events SEC(".maps");SEC("tracepoint/syscalls/sys_enter_execve")
int tracepoint_execve(struct syscall_trace_enter *ctx) {u64 id = bpf_get_current_pid_tgid();pid_t pid = (pid_t)id;pid_t tgid = id >> 32;if(bpf_map_update_elem(&execs, &pid, &empty_event, BPF_NOEXIST)) {return 0;}struct event *event = bpf_map_lookup_elem(&execs, &pid);if(event == NULL) {return 0;}event->pid = tgid;bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, event, sizeof(struct event));bpf_map_delete_elem(&execs, &pid);return 0;
}char _license[] SEC("license") = "GPL";
用户态代码:
#include <stdio.h>
#include <errno.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "execve.h"#define PERF_BUFFER_PAGES 64
#define PERF_POLL_TIMEOUT_MS 100static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz) {const struct event *e = data;printf("pid:%d\n", e->pid);
}int main() {int err;// 打开内核程序生成的二进制文件struct bpf_object *bpf_obj = bpf_object__open("execve_kern.o");if(bpf_obj == NULL) {printf("open .o faile failed\n");return -1;}// 载入内核程序int ret = bpf_object__load(bpf_obj);if(ret != 0) {printf("load bpf object failed\n");return -1;}// 根据内核函数的函数名找到内核程序struct bpf_program *bpf_prog = bpf_object__find_program_by_name(bpf_obj, "tracepoint_execve");struct bpf_link *bpf_link = bpf_program__attach(bpf_prog);if(libbpf_get_error(bpf_link)) {return -1;}// 找到事件的mapint fd = bpf_object__find_map_fd_by_name(bpf_obj, "events");// 根据事件的map创建perf_buffer,并指定perf_buffer中的数据的回调处理函数handle_eventstruct perf_buffer *pb = NULL;pb = perf_buffer__new(fd, PERF_BUFFER_PAGES, handle_event, NULL, NULL, NULL);if(pb == NULL) {printf("open perf buffer failed\n");goto cleanup;}while(true) {err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);if(err < 0 && err != -EINTR) {printf("poll failed\n");goto cleanup;}err = 0;}cleanup:perf_buffer__free(pb);bpf_link__destroy(bpf_link);bpf_object__close(bpf_obj);return 0;
}
- 编译内核态程序:clang -g -target bpf -D__TARGET_ARCH_x86 -c execve_kern.c -o execve_kern.o
- 编译用户态程序:gcc -g -Wall -c execve_user.c -o execve_user.o
- 将用户态程序编译为完整的二进制:gcc -g -Wall execve_user.o -o execve -lbpf
执行./execve时,如果有新的进程就会打印进程的Pid。
5 基于libbpf-bootstrap脚手架
基于libbpf的方式有个缺点:最终的二进制包含内核态程序编译的二进制execve_kern.o和用户态程序编译的二进制execve,不便于分发。
第二种方式也是基于libbpf库,只是在上层做了一些封装,并且可以整体打成一个二进制。
git clone https://github.com/libbpf/libbpf-bootstrap# 下载依赖的仓库的代码
cd libbpf-bootstrap && git submodule update --init --recursive
libbpf-bootstrap仓库本身的内容只有examples(示例代码),以及与vmlinux.h相关的tools和vmlinux目录,主要依赖其他的仓库:
- blazesym:地址符号化和反向查找
- bpftool:用于生成skel文件和vmlinux.h文件
- libbpf:也就是上面的libbpf库
examples/c中的minimal.bpf.c和minimal.c就是最简单的示例代码。
// minimal.bpf.c
#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 handle_tp(void *ctx)
{int pid = bpf_get_current_pid_tgid() >> 32;if (pid != my_pid)return 0;bpf_printk("BPF triggered from PID %d.\n", pid);return 0;
}
内核态代码hook write系统调用,当用户态程序执行write时,就打印一行输出,这里的my_pid会在用户态程序中被替换。
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "minimal.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 minimal_bpf *skel;int err;/* 安装libbpf库的错误和调试函数 */libbpf_set_print(libbpf_print_fn);/* 打开内核态程序 */skel = minimal_bpf__open();if (!skel) {fprintf(stderr, "Failed to open BPF skeleton\n");return 1;}/* 将当前进程的pid更新到内核态程序中 */skel->bss->my_pid = getpid();/* 加载和验证内核态程序 */err = minimal_bpf__load(skel);if (err) {fprintf(stderr, "Failed to load and verify BPF skeleton\n");goto cleanup;}/* 将内核态程序附着到挂载点 */err = minimal_bpf__attach(skel);if (err) {fprintf(stderr, "Failed to attach BPF skeleton\n");goto cleanup;}// 打印一行提示,可以通过查看/sys/kernel/debug/tracing/trace_pipe文件内容查看内核程序的输出printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` ""to see output of the BPF programs.\n");for (;;) {/* 执行下面的fprintf会调用write系统调用,就会触发内核态挂载点函数的执行 */fprintf(stderr, ".");sleep(1);}cleanup:minimal_bpf__destroy(skel);return -err;
}
对上述代码进行编译:
make minimal
会在当前路径下生成minimal二进制,执行minimal就可以看到效果,此时,内核态二进制代码也在minimal二进制中。
6 libbpf-boostrap中示例的编译过程
在执行make minimal时会看到整体的编译过程:
- 创建本地的编译目录:.output
- 编译libbpf:编译libbpf-bootstrap/libbpf,并将中间文件和最终生成.a文件放到.output/libbpf中
- 编译bpftool:编译libbpf-bootstrap/bpftool
- 将minimal.bpf.c编译为minimal.bpf.o
- 使用bpftool工具将minimal.bpf.o转换成minimal.skel.h
- 将minimal.c编译为minimal.o
- 生成最终的二进制minimal
但是,上述命令只能看到大概的编译过程,如果想看到执行编译的完整命令,可以执行make minimal -e V=1。
例如,编译过程中会打印以下信息:
... libbfd: [ OFF ]
... clang-bpf-co-re: [ on ]
... llvm: [ OFF ]
... libcap: [ OFF ]
其实是因为在编译bpftool过程中对上述库进行分别测试。
最终编译minimal的代码使用了以下命令:
# 使用clang将minimal.bpf.c编译为minimal.tmp.bpf.o
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 \-I.output -I../../libbpf/include/uapi -I../../vmlinux/x86/ -I/root/ebpf/libbpf-bootstrap/blazesym/capi/include -idirafter /usr/bin/../lib/clang/18/include -idirafter /usr/local/include -idirafter /usr/include \-c minimal.bpf.c -o .output/minimal.tmp.bpf.o# 使用bpftool将minimal.bpf.o编译为minimal.tmp.bpf.o
/root/ebpf/libbpf-bootstrap/examples/c/.output/bpftool/bootstrap/bpftool gen object .output/minimal.bpf.o .output/minimal.tmp.bpf.o# 使用bpftool将minimal.bpf.o转换成minimal.skel.h
/root/ebpf/libbpf-bootstrap/examples/c/.output/bpftool/bootstrap/bpftool gen skeleton .output/minimal.bpf.o > .output/minimal.skel.h# 使用gcc将minimal.c编译为minimal.o
cc -g -Wall -I.output -I../../libbpf/include/uapi -I../../vmlinux/x86/ -I/root/ebpf/libbpf-bootstrap/blazesym/capi/include -c minimal.c -o .output/minimal.o# 使用gcc将minimal.o编译为minimal
cc -g -Wall .output/minimal.o /root/ebpf/libbpf-bootstrap/examples/c/.output/libbpf.a -lelf -lz -o minimal
在最终生成minimal二进制的过程中,主要依赖三个库:libbpf、libelf、libzlib,其中,只有libbpf是静态库,另外两个都是动态库。
7 参考文档
- perf-tools:基于ftrace和perf开发的性能分析工具
- libbpf-tools:基于libbpf开发的工具
- BPF Performance Tools:
BPF性能之巅的配套代码 - eBPF动手实践系列三:基于原生libbpf库的eBPF编程改进方案
- libbpf-bootstrap:libbpf的脚手架项目
- BPF Documentation:Linux内核中的BPF文档
- BPF Portability and CO-RE
- elfutils
相关文章:
eBPF编程指南(一):eBPF初体验
1 什么是EBPF? EBPF是一种可以让程序员在内核态执行自己的程序的机制,但是,为了安全起见,无法像内核模块一样随意调用内核的函数,只能调用一些bpf提前定义好的函数。为了让内核执行程序员自己的代码,需要指…...
pip笔记
pip介绍 pip的全称:package installer for python,也就是Python包管理工具。 配置镜像源 镜像列表 阿里云 http://mirrors.aliyun.com/pypi/simple/中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/豆瓣 http://pypi.douban.com/simple/清华大…...
centos安装postgresql-12
安装pg文件 sudo curl -o /etc/yum.repos.d/pgdg-redhat-all.repo https://mirrors.aliyun.com/postgresql/repos/yum/12/redhat/rhel-7-x86_64/pgdg-redhat-all.repo 清楚缓存重新安装 sudo yum clean all sudo yum makecache 如果报错 删除现有的文件 sudo rm /etc/yum.r…...
Npm使用教程
Npm使用教程 Npm(Node Package Manager)是Node.js的包管理工具和软件包管理系统,广泛用于JavaScript项目的依赖管理和包发布。本文将为你提供一份详细的Npm使用教程,从安装、基本命令、包管理到高级用法,帮助你全面掌…...
【Android Studio】修改项目名称can‘t rename root module解决办法
文章目录 问题现象解决办法 问题现象 修改项目名称 但是直接rename 又会出现 can‘t rename root module 的警告 下图方式只适合修改除项目级别以外的,直接修改项目名称则会报错 解决办法 此时我们只要两步就可以成功修改项目名称了 关闭项目修改其文件夹名称…...
豆瓣Top250电影数据分析可视化系统(Flask+Mysql+Pyecharts)
爬取目标网址:豆瓣Top250 可以看到进入每条电影的详细链接后,显示的内容会更加详细一点 因此我们需要先利用爬虫技术从主页爬取到每条电影的链接,再分别遍历每条电影的链接,获取它的详细内容,这里仅展示一部分代码 利…...
软件质量保证计划书(2024Word完整版)
软件质量保证计划书要点:确立质量目标,组建质保团队,规划全程质控活动,设定质量标准,明确各阶段检查与评审流程,确保各角色职责清晰。强化过程监控,注重数据度量,旨在通过持续改进&a…...
【学习笔记】Matlab和python双语言的学习(动态规划)
文章目录 前言一、动态规划动态规划的基本步骤示例1示例2 三、代码实现----Matlab1.示例12.示例2 四、代码实现----python1.示例12.示例2 总结 前言 通过模型算法,熟练对Matlab和python的应用。 学习视频链接: https://www.bilibili.com/video/BV1EK411…...
低代码开发:机遇与挑战的双重探索
随着科技的迅速发展,“低代码”开发平台悄然兴起,为非专业程序员提供了构建应用程序的快捷途径。无疑,这一创新技术正在颠覆传统的软件开发模式,并激发了IT行业的热烈讨论。但究竟低代码平台是提高开发效率的有利工具,…...
Docker最佳实践(三):安装mysql
大家好,欢迎各位工友。 本篇呢我们就来演示一下如何在Docker中部署MySQL容器,可以按照以下步骤进行: 1. 搜索镜像 首先搜索MySQL镜像,可以使用以下命令: docker search mysql2. 拉取镜像 根据需求选择MySQL或Maria…...
进阶SpringBoot之 Web 静态资源导入
idea 创建一个 web 项目 新建 controller 包下 Java 类,用来查验地址是否能成功运行 package com.demo.web.controller;import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;RestControl…...
【数据结构七夕专属版】单链表及单链表的实现【附源码和源码讲解】
本篇是博主在学习数据结构时的心得,希望能够帮助到大家,也许有些许遗漏,但博主已经尽了最大努力打破信息差,如果有遗漏还请见谅,嘻嘻,前路漫漫,我们一起前进!!࿰…...
鸿蒙笔记--Socket
这一节主要了解鸿蒙Socket通信,在鸿蒙系统中,Socket TCP通讯是一种常用的网络通信方式,它提供了可靠的、面向连接的数据传输服务。它主要用到ohos.net.socket这个库; constructTCPSocketInstance:创建一个 TCPSocket;…...
安装python+python的基础语法
安装python python2为内置,安装python3----3.6.8 最新安装3.12使用源码安装 1.查看yum源,epel [rootpython01 ~]# yum list installed |grep epel 2.安装python3 [rootpython01 ~]# yum -y install python3 3.查看版本 [rootpython01 ~]# python…...
html+css网页制作 国家体育总局2个页面模版(无js)
htmlcss网页制作 国家体育总局2个页面模版(无js) 网页作品代码简单,可使用任意HTML编辑软件(如:Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&a…...
Effective Java学习笔记第27、28条原生态类型和非受检警告
目录 什么是泛型 泛型与编译器 不要轻易使用原生态类型 可以通过通配符类型来替代原生态类型 几个适合原生态类型的场景 消除非受检的警告 什么是非受检警告 如果无法消除警告 本书27-33条主要介绍泛型。首先介绍什么是泛型,它的应用场景是什么。然后重点介…...
javaEE和javaSE
引用自:https://developer.baidu.com/article/detail.html?id3312755 文章目录 前景描述javaSE简介使用场景 javaEE(J2EE)简介使用场景 结语 前景描述 javaEE和javaSE是java中比较常见的两个概念,但是又比较容易忘记,在此进行记…...
Leetcode 17.电话号码的字母组合
目录 题目 方法一 思路 代码 题目 17. 电话号码的字母组合 难度:中等 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对…...
位1的个数
编写一个函数,获取一个正整数的二进制形式并返回其二进制表达式中设置位的个数(也被称为汉明重量)。 示例 1: 输入:n 11 输出:3 解释:输入的二进制串 1011 中,共有 3 个设置位。示…...
RPA在政务服务中的挑战与解决方案
随着数字化时代的到来,数字政务的建设已成必然趋势,RPA作为数字化转型的重要工具之一,能够帮助政府单位快速实现业务流程的自动化和智能化,提高工作效率和质量,为建设数字政务提供强有力的支持,因此正被越来…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)
Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败,具体原因是客户端发送了密码认证请求,但Redis服务器未设置密码 1.为Redis设置密码(匹配客户端配置) 步骤: 1).修…...
JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...
IP如何挑?2025年海外专线IP如何购买?
你花了时间和预算买了IP,结果IP质量不佳,项目效率低下不说,还可能带来莫名的网络问题,是不是太闹心了?尤其是在面对海外专线IP时,到底怎么才能买到适合自己的呢?所以,挑IP绝对是个技…...
【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...
如何更改默认 Crontab 编辑器 ?
在 Linux 领域中,crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用,用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益,允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...
作为测试我们应该关注redis哪些方面
1、功能测试 数据结构操作:验证字符串、列表、哈希、集合和有序的基本操作是否正确 持久化:测试aof和aof持久化机制,确保数据在开启后正确恢复。 事务:检查事务的原子性和回滚机制。 发布订阅:确保消息正确传递。 2、性…...
苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会
在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...
