好久没有更新文章了,最近在学习bpf,今天把学习的路径整理记录下。
ebpf简介
ebpf常用的场景有:
- 网络相关功能:网络数据包过滤、流量监控与统计、负载均衡;
- 性能分析功能:系统调用跟踪、函数调用跟踪、CPU 和内存使用分析;
- 安全相关功能:进程和文件访问控制、恶意行为检测;
从上面分类可以看出ebpf应用场景十分广泛,对工作中带来了很多便利。可以安全且高效地扩展内核功能,而无需更改内核源代码或加载内核模块。
不过在实际应用中会遇到很多词汇libbpf、libbpf-tools、bpftools、bcc、bpftrace等。这些工具关联是什么,怎么使用。本人刚开始也是存在很多疑惑,解析来通过实验逐一介绍。
helloworld
// hello.bpf.c
#include
#include
SEC("tracepoint/syscalls/sys_enter_write")
void hello(struct trace_event_raw_sys_enter *ctx) {
bpf_printk("Hello, World!\n");
}
char _license[] SEC("license") = "GPL";
// hello_user.c
#include
#include
int main(int argc, char **argv) {
struct bpf_object *obj;
// 打开并加载 eBPF 对象
obj = bpf_object__open_file("hello.bpf.o", NULL);
// 加载 eBPF 对象
bpf_object__load(obj);
// 附加 eBPF 程序到 tracepoint
bpf_program__attach_tracepoint(bpf_object__find_program_by_name(obj, "hello"), "syscalls", "sys_enter_execve");
// 等待一段时间,让 eBPF 程序有机会执行
sleep(10);
// 关闭 eBPF 对象
bpf_object__close(obj);
return 0;
}
程序分为2部分:kernel(hello.bpf.c)和user(hello_user.c)。
- hello.bpf.c:该文件会被编译成bpf字节码,被load到kernel中并hook到对应的函数上,当对应函数被执行时就会触发定义好的功能(如hello);
- hello_user.c:该文件是用于控制步骤1中被编译好的二进制的生命周期,包括:open、load、attach、close。
下面具体编译如下:
#clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I/usr/include/x86_64-linux-gnu -c hello.bpf.c -o hello.bpf.o
#gcc -o hello_user hello_user.c -lbpf -lelf -lz
// 运行
#./hello_user
// 查看输出
#cat /sys/kernel/debug/tracing/trace
libbpf
从上面代码中引出第一个介绍的对象libbpf:libbpf 是一个基于C的库,包含一个BPF加载器,可以将已编译的BPF对象文件加载到Linux内核中。另外libbpf还包含加载、验证以及将BPF程序附加到各种内核钩子的工作。
libbpf操作BPF生命周期的各个阶段:
- open:对应bpf_object__open_file接口,libbpf解析BPF对象文件并设置BPF映射、BPF程序和全局变量。
- load:对应bpf_object__load接口,libbpf创建BPF映射、解析、验证并将BPF程序加载到内核中,但并不执行任何BPF程序。
- attach:对应bpf_program__attach_xxx接口,libbpf 将BPF程序附加到各种BPF钩子点(如tracepoint、kprobes等)。当钩子函数触发时会收集数据供用户空间读取。
- close(destory):对应bpf_object__close接口。从内核卸载BPF对象并释放资源。
user操作bpf对象的流程基本是固定的,如果开发人员只关注kernel部分,就可以大大提升效率。借助bpftool工具可以完成bpf程序的加载。
bpftool
bpftool是一款功能强大且非常实用的命令行工具,目前已经是linux源码的一部分,路径在tools/bpf/bpftool 子目录下,并且会定期同步到github上。这里只介绍替代user简化bpf程序开发的作用。
可以通过以下命令进行加载:
bpftool prog load hello.bpf.o /sys/fs/bpf/hello autoattach
对命令简单说明下:首先同样需要把xxx.bpf.c编译成xxx.bpf.o,通过load和attach加载到内核并且自动hook到钩子函数。这里使用了虚拟文件系统bpf,可以把bpf程序持久化到系统中,如果存在多个bpf程序可以相互共享数据,如果不使用bpf文件系统,使用user程序load的方式退出后便会释放掉资源无法再次使用。
通过bpftool简化了程序的开发,不过有人可能会问是否可以把kernel和user编译成一个二进制,而不借用其他工具进行加载,答案是肯定的。首先需要介绍下BPF skeleton文件。
skeleton
骨架的意思,即为方便用户开发BPF程序封装的通用libbpf API模板。模板中包括BPF字节码、BPF对象操作集,skeleton操作集中的方法命名规则是固定的:如xxx__open、xxx__load等。下面是user程序使用模板改造后的,可以直接编译成二进制运行:
// hello.c
#include
#include
#include
#include
#include
#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_bpf *skel;
libbpf_set_print(libbpf_print_fn);
skel = hello_bpf__open_and_load();
hello_bpf__attach(skel);
sleep(10);
hello_bpf__destroy(skel);
}
而hello.skel.h就是抽取出来的模板文件,可以使用bpftool把xxx.bpf.o转化成xxx.skel.h文件,那么user程序只需要包含xxx.skel.h头文件。
bpftool gen skeleton hello.bpf.o > hello.skel.h
现在梳理下使用skeleton编程流程,如下图:
- 开发人员需要编写bpf kernel程序;
- 使用clang编译成xxx.bpf.o,然后借用bpftool转化成xxx.skel.h;
- 引用xxx.skel.h编写bpf user程序,由于user程序基本是固定的,因此可以提前写好一个模板,一本万利!
如果开发人员每次都按照以上3个步骤也比较麻烦,是否可以进一步简化开发流程,答案也是肯定的,接下来介绍ebpf开发脚手架:eunomia-bpf
eunomia-bpf
eunomia-bpf是一个开源的 eBPF编译工具链和运行时框架,旨在简化 eBPF 程序的开发。它通过提供高层次的抽象和工具链支持,使得开发者能够更加高效地使用 eBPF 技术来监控、分析和优化系统性能。
通过eunomia对上面流程进行简化,可以大大缩减开发时间成本(x86环境,如果是arm64可以官网直接下载):
先下载2个工具:
- ecc用于把源码编译打包成package.json文件;
- ecli用于加载package.json。
// 下载工具
# wget https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecc && chmod +x ./ecc
# wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli
// 编译 & 运行
#./ecc hello.bpf.c
#./ecli package.json
有人受bcc启发,基于libbpf和eunomia-bpf重新编写了一套C工具集合并提供了对应的教程:bpf-developer-tutorial。从初级到高级一系列教程值得去学习下,可以深入的理解和使用bpf。
bcc
使用过bpf的大多数都听过BCC,特别是bcc提供的工具集,如execsnoop、tcpconnect等众多实用的工具。 bcc封装了用于python、c++和lua语言开发的库, 开发者利用BCC提供的编程接口编写 BPF 程序可以更高效,特别是前端采用python语言,而且bcc提供的大量工具都是使用的python语言编写的。由于此文章只是入门级,不介绍过多实现原理。
BCC同时也有一些缺点,由于依赖运行时编译,因此需要内嵌LLVM/Clang 库,体积会比较大;编译期间占用大量资源(内存和 CPU),依赖于内核头文件库,其必须安装在每个目标主机上。
libbpf-tools
从名字可以猜测libbpf-tools只是基于libbpf编写的工具集。bcc如果只充当编译器功能,同样可以支持使用libbpf编程。libbpf-tools是bcc的子目录,结合了libbpf和CO-RE技术编写的可移植性工具集。在系统资源有限的移动设备,可以直接使用编译好的libbpf-tools工具,编译方法如下:
# git clone --recurse-submodules https://github.com/iovisor/bcc.git
# cd bcc/libbpf-tools/src
# make
bpftrace
bpftrace 是一个基于bpf和bcc的开源跟踪器,还提供一种高级编程脚本语言bt,bpftrace 使用LLVM将脚本编译为eBPF字节码,并利用libbpf和bcc与Linux的BPF子系统进行交互。和bcc相比,bpftrace语法更简单,可以使用单行工具满足常规需求,bt脚本更容易上手;而bcc需要较高的编程能力,同时可以完成更加复杂的工作。
本人也提供了快速编译arm64架构静态bpftrace的方法,可以参考这里,也可以直接下载使用。
libbpf-bootstrap
libbpf-bootstrap是使用Libbpf+CO-RE编写bpf程序的脚手架,提供了一定的模板,开发人员只需要按照模板编写xxx.bpf.c和xxx.c即可,简单的bpf程序甚至可以省略xxx.c。接下来还是以上面的hello程序为例进行说明libbpf-bootstrap的使用方法:
- 下载libbpf-bootstrap代码:
# git clone --recurse-submodules https://github.com/libbpf/libbpf-bootstrap.git
- 安装依赖
# apt install clang libelf1 libelf-dev zlib1g-dev
- 把hello.bpf.c和skeleton部分hello.c放在libbpf-bootstrap/examples/c目录下,在Makefile中添加hello程序:
-- a/examples/c/Makefile
+++ b/examples/c/Makefile
@@ -25,8 +25,8 @@ CFLAGS := -g -Wall
ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
APPS = minimal minimal_legacy minimal_ns bootstrap uprobe kprobe fentry \
- usdt sockfilter tc ksyscall task_iter lsm
+ usdt sockfilter tc ksyscall task_iter lsm hello
4.编译 & 运行:
#cd libbpf-bootstrap
# mkdir build && cd build
# cmake ../examples/c
# ./hello
总结
上面介绍了开发bpf的2种方式:libbpf +CO-RE 和bcc。
libbpf-bootstrap、bpftool和eunomia-bpf是为了使用ibbpf开发更加方便,而libbpf-tools、bpftrace是基于libbpf开发的应用工具。个人更喜欢使用libbpf-bootstrap开发bpf。