eBPF学习笔记(一)—— eBPF介绍&内核编译
- eBPF介绍
- eBPF基础
- 代码验证
- 即时编译
- eBPF开发工具
- BCC
- bpftrace
- libbpf C/C++库
- eBPF Go库
- 内核编译
- 查看内核版本
- 下载内核源码
- 安装依赖项
- 安装最新版llvm和clang
- 配置内核
- 编译内核BPF示例程序
- 常见问题
- 问题一:libbpf: map 'rx_cnt': unexpected def kind var.
- 问题二:/bin/sh: 1: scripts/mod/modpost: not found
- 第一个eBPF程序
- hello_kern.c
- hello_user.c
- Makefile
- 编译
- 运行
- 参考资料
eBPF介绍
BPF(Berkeley Packet Filter,拓展的伯克利报文过滤器)
是类Unix系统上数据链路层的一种原始接口,提供原始链路层封包的收发。除此之外,如果网卡驱动支持混杂模式,那么它可以让网卡处于此种模式,这样可以收到网络上的所有包,不管他们的目的地是不是所在主机,目前被称为cBPF(classical BPF)。
从3.18版本开始,Linux 内核提供了一种扩展的BPF虚拟机,名为“extended BPF”,简称为eBPF
。它能够被用于非网络相关的功能,比如附在不同的tracepoints上,从而获取当前内核运行的许多信息。
eBPF现在主要被应用于网络、跟踪、内核优化、硬件建模等领域。
eBPF基础
eBPF 程序是事件驱动的,能将代码加载进内核中的eBPF虚拟机,并在内核或应用程序通过某个挂钩点时运行。 预定义的钩子包括系统调用、函数进入/退出、内核跟踪点、网络事件等。
如果不存在用于特定需求的预定义挂钩,则可以创建内核探针 (kprobe) 或用户探针 (uprobe) 以在内核或用户应用程序的几乎任何位置附加 eBPF 程序。
当程序加载到 Linux 内核中时,它在附加到请求的钩子之前要经过两个步骤:
- 代码验证
- 即时编译(Just In Time,JIT)
代码验证
验证步骤确保 eBPF 程序可以安全运行。 它验证程序是否满足几个条件,例如:
- 加载 eBPF 程序的进程拥有所需的能力(特权)。
除非启用非特权 eBPF,否则只有特权进程才能加载 eBPF 程序。 - 该程序不会崩溃或以其他方式损害系统。
- 程序总是运行到完成(即程序不会永远处于循环中,阻止进一步的处理)。
即时编译
即时 (JIT) 编译步骤将程序的通用字节码转换为机器特定的指令集,以优化程序的执行速度,这使得 eBPF 程序可以像本地编译的内核代码或作为内核模块加载的代码一样高效地运行。
eBPF开发工具
目前eBPF存在几个开发工具链来协助 eBPF 程序的开发和管理,用于满足用户的不同需求:
- BCC
- bpftrace
- libbpf C/C++库
- eBPF Go库
BCC
BCC 是一个框架,使用户能够编写带有嵌入其中的 eBPF 程序的 python 程序。 该框架主要针对涉及应用程序和系统分析/跟踪的用例,其中 eBPF 程序用于收集统计信息或生成事件,而用户空间中的对应物收集数据并以人类可读的形式显示。 运行 python 程序将生成 eBPF 字节码并将其加载到内核中。
bpftrace
bpftrace 是 Linux eBPF 的高级跟踪语言,可用于最新的 Linux 内核。 bpftrace 使用 LLVM 作为后端将脚本编译为 eBPF 字节码,并利用 BCC 与 Linux eBPF 子系统以及现有的 Linux 跟踪功能进行交互:内核动态跟踪 (kprobes)、用户级动态跟踪 (uprobes) 和跟踪点。bpftrace 语言的灵感来自 awk、C 和前身跟踪器,例如 DTrace 和 SystemTap。
libbpf C/C++库
libbpf 库是一个基于 C/C++ 的通用 eBPF 库,它有助于将由 clang/LLVM 编译器生成的 eBPF 目标文件加载到内核中,并且通常通过提供易于使用的库 API 来抽象与 BPF 系统调用的交互应用程序。
eBPF Go库
eBPF Go 库提供了一个通用的 eBPF 库,它将获取 eBPF 字节码的过程与 eBPF 程序的加载和管理分离。 eBPF 程序通常是通过编写高级语言创建的,然后使用 clang/LLVM 编译器编译为 eBPF 字节码。
内核编译
本篇将介绍如何编译高版本Linux内核源码中的eBPF示例代码并用其编写自定义例程。
查看内核版本
root@ubuntu:~# uname -r
5.15.0-52-generic
下载内核源码
点此下载与当前系统内核版本一致的内核源码,并解压到/usr/src目录中:
root@ubuntu:/usr/src# ls
linux-5.15
安装依赖项
apt install libncurses5-dev flex bison libelf-dev binutils-dev libssl-dev libcap-dev
安装最新版llvm和clang
1)备份源文件
cp /etc/apt/sources.list /etc/apt/sources.list.bak
2)编辑/etc/apt/sources.list,在末尾加入以下内容,保存
deb http://apt.llvm.org/focal/ llvm-toolchain-focal main
deb-src http://apt.llvm.org/focal/ llvm-toolchain-focal main
deb http://apt.llvm.org/focal/ llvm-toolchain-focal-12 main
deb-src http://apt.llvm.org/focal/ llvm-toolchain-focal-12 main
deb http://apt.llvm.org/focal/ llvm-toolchain-focal-13 main
deb-src http://apt.llvm.org/focal/ llvm-toolchain-focal-13 main
3)更新源
apt update
4)安装llvm
apt install llvm
5)安装clang
apt install clang
更新源的时候可能会出现这个错误:
使用以下命令解决:
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 15CF4D18AF4F7421
配置内核
1)进入内核源码目录,后续操作都在源码根目录进行:
cd linux-5.15
2)拷贝vmlinux至当前目录
cp /sys/kernel/btf/vmlinux ./vmlinux
3)在内核源码根目录下生成.config文件
make defconfig
编译内核BPF示例程序
1)关联内核头文件
make headers_install
2)编译
make M=samples/bpf
如果出现以下警告,不影响编译:
常见问题
问题一:libbpf: map ‘rx_cnt’: unexpected def kind var.
解决方案:升级llvm和clang至最新版即可
问题二:/bin/sh: 1: scripts/mod/modpost: not found
解决方案:
make modules_prepare
第一个eBPF程序
文件目录:/usr/src/linux-5.15/samples/bpf
注意:自Linux内核5.11版本开始,移除了bpf_load.h等头文件,部分函数和宏不再适用,因此在代码的写法上也有不同,网上的demo大多是基于5.10版本以下的,不适用于新版内核,本篇在这里提供新版本写法
hello_kern.c
#include <bpf/bpf_helpers.h>
#define SEC(NAME) __attribute__((section(NAME), used))
SEC("tracepoint/syscalls/sys_enter_execve") //触发的事件类型/系统调用
int bpf_prog(void *ctx) //触发事件时调用的函数
{
char msg[] = "Hello BPF World!";
bpf_trace_printk(msg, sizeof(msg)); //将信息写入管道,等待用户态读取
return 0;
}
char _license[] SEC("license") = "GPL";
hello_user.c
#include <stdio.h>
#include "trace_helpers.h"
int main(int ac, char **argv)
{
struct bpf_link *link = NULL;
struct bpf_program *prog;
struct bpf_object *obj;
char filename[256];
snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]); //只有当运行的程序名为hello时才能读到hello_kern.o
obj = bpf_object__open_file(filename, NULL);
if (libbpf_get_error(obj)) {
fprintf(stderr, "ERROR: opening BPF object file failed\n");
return 0;
}
prog = bpf_object__find_program_by_name(obj, "bpf_prog");
if (!prog) {
fprintf(stderr, "ERROR: finding a prog in obj file failed\n");
goto cleanup;
}
/* load BPF program */
if (bpf_object__load(obj)) {
fprintf(stderr, "ERROR: loading BPF object file failed\n");
goto cleanup;
}
link = bpf_program__attach(prog);
if (libbpf_get_error(link)) {
fprintf(stderr, "ERROR: bpf_program__attach failed\n");
link = NULL;
goto cleanup;
}
read_trace_pipe();
cleanup:
bpf_link__destroy(link);
bpf_object__close(obj);
return 0;
}
Makefile
编辑samples/bpf目录中的Makefile文件,在下方标红位置添加内容:
…
tprogs-y += xdp_redirect_map
tprogs-y += xdp_redirect
tprogs-y += xdp_monitor
tprogs-y += hello
…
xdp_redirect_map-objs := xdp_redirect_map_user.o $(XDP_SAMPLE)
xdp_redirect-objs := xdp_redirect_user.o $(XDP_SAMPLE)
xdp_monitor-objs := xdp_monitor_user.o $(XDP_SAMPLE)
hello-objs := hello_user.o $(TRACE_HELPERS)
…
always-y += hbm_out_kern.o
always-y += hbm_edt_kern.o
always-y += xdpsock_kern.o
always-y += hello_kern.o
…
编译
回到内核源码根目录,再次运行以下命令:
make M=samples/bpf
如果出现如下图红框中的内容,不影响编译:
查看是否编译成功:
ls | grep "hello*"
运行
1)运行前线新起一个终端,用于测试:
2)在终端1中运行编译好的程序:
3)在终端2中执行任意命令,在终端1查看程序是否能够监测到,如果成功监测到新进程运行便会输出一条"Hello BPF World"
参考资料
- eBPF文档 - What is eBPF?
- bilibili - 高效入门eBPF
- bilibili - BPF C编程入门
- Ubuntu20安装最新版clang
- 深入浅出 eBPF - 基于 Ubuntu 21.04 BPF 开发环境全攻略