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作为数字化转型的重要工具之一,能够帮助政府单位快速实现业务流程的自动化和智能化,提高工作效率和质量,为建设数字政务提供强有力的支持,因此正被越来…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...

Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...

蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1):从基础到实战的深度解析-CSDN博客,但实际面试中,企业更关注候选人对复杂场景的应对能力(如多设备并发扫描、低功耗与高发现率的平衡)和前沿技术的…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
以下是一个完整的 Angular 微前端示例,其中使用的是 Module Federation 和 npx-build-plus 实现了主应用(Shell)与子应用(Remote)的集成。 🛠️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...
【JavaSE】多线程基础学习笔记
多线程基础 -线程相关概念 程序(Program) 是为完成特定任务、用某种语言编写的一组指令的集合简单的说:就是我们写的代码 进程 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存…...