编写eBPF程序,通常通过Cilium、bcc或bpftrace等项目间接使用,这些项目提供eBPF抽象,不需要直接编写程序,而是使用预定义功能和帮助函数实现eBPF相关功能。相关框架利用bpf系统调用将eBPF程序加载到Linux内核中。
1.libbpf-bootstrap
使用libbpf-bootstrap脚手架,可以快速、轻松地构建 BPF 应用程序。
$ tree
.
├── libbpf
│ ├── ...
│ ...
├── LICENSE
├── README.md
├── src
│ ├── bootstrap.bpf.c
│ ├── bootstrap.c
│ ├── bootstrap.h
│ ├── Makefile
│ ├── minimal.bpf.c
│ ├── minimal.c
│ ├── vmlinux_508.h
│ └── vmlinux.h -> vmlinux_508.h
└── tools
├── bpftool
└── gen_vmlinux_h.sh
16 directories, 85 files
目前有两个BPF 应用演示程序:minimal 和 bootstrap。
2.minimal
minimal执行后打印输出Hello World,且不使用 BPF CO-RE。
2.1 内核态BPF代码
BPF 内核态代码(minimum.bpf.c) :
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
linux/bpf.h 包括一些基本的 BPF 相关类型和使用内核 BPF API 所必需的常量。(例如,BPF 辅助函数标志)
bpf/bpf_helpers.h 包括libbpf最常用的宏、常量和 BPF help类的API定义。
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC()(由bpf_helpers.h提供)将变量和函数放入指定的部分。
SEC("license"),以及其他一些部分名称,是由libbpf约定。
LICENSE变量定义了 BPF 代码的许可证,指定许可证是强制性的。某些 BPF 功能对非 GPL 的代码不可用。
int my_pid = 0;
int my_pid = 0:定义了一个全局变量,BPF 代码可以读取和更新变量值。 Linux 5.5 版本以后,可以从用户空间读取和写入全局变量。全局变量经常用于配置 BPF 应用程序的额外设置。也可以用于在内核 BPF 代码和用户空间代码之间传递数据。
SEC("tp/syscalls/sys_enter_write")
SEC("tp/syscalls/sys_enter_write")用于定义被加载到内核中的 BPF 程序的类型。tp/syscalls/sys_enter_write部分定义了 libbpf 应该创建什么类型的 BPF 程序以及它可以在内核中附加的方式/位置。
minimal定义了一个跟踪点 BPF 程序,每次从任何用户空间进程调用write()系统调用时都会调用该BPF程序。
在同一个 BPF C 代码文件中可能定义了许多 BPF 程序。它们可以是不同类型(即SEC()注释)。例如,可以有几个不同的 BPF 程序,每个程序用于不同的跟踪点或其他一些内核事件(例如,正在处理的网络数据包等)。还可以定义多个具有相同SEC()属性的 BPF 程序。
int handle_tp(void *ctx)
{
int my_pid = 0;
int pid = bpf_get_current_pid_tgid() >> 32;
if (pid != my_pid)
return 0;
bpf_printk("BPF triggered from PID %d.\n", pid);
bpf_printk("Hello World!");
return 0;
}
bpf_get_current_pid_tgid()的返回值是高 32 位编码的 PID(即TGID线程组 ) 。右移32位后得到PID,用于检查触发write()系统调用的进程是否是我们的minimal BPF的进程。因为很可能很多进程都会调用write()。
my_pid全局变量将对minimal用户态代码执行时中进程的实际 PID 进行初始化。
bpf_printk("BPF triggered from PID %d.\n", pid);
bpf_printk()将格式化的字符串发送到虚拟文件系统:
/sys/kernel/debug/tracing/trace_pipe,可以 cat 从控制台查看其内容。
由于目前还没有 BPF 调试器,bpf_printk()这通常是调试 BPF 代码中问题的最快和最方便的方法。
2.2 用户态C代码
用户空间(minimum.c)
#include "minimal.skel.h"
minimal.skel.h头文件包括 BPF 代码的 BPF 框架minimal.bpf.c。它由 bpftool 在 Makefile 中自动生成的,它还将编译后的 BPF 目标代码的内容嵌入头文件中来简化 BPF 代码部署工作,该头文件包含在用户态代码中。无需在应用程序二进制文件中部署额外的文件,只需包含标头就可以了。
src/.output/minimal
bpf_object *ob j结构体可以传递maps、progs和links给 libbpf API 函数。
比如,提供对 BPF 映射和 BPF 代码中定义的程序(例如,handle_tp BPF 程序)的直接访问。
Skeleton 还可以选择具有允许从用户空间直接(不需要额外的系统调用)访问 BPF 全局变量的bss、data部分。
用户空间(minimum.c)main函数:
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
libbpf_set_print()为所有 libbpf 日志提供自定义回调,可以打印 libbpf 调试日志。默认情况下,libbpf 将仅记录错误级别的消息。
/*Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything*/
bump_memlock_rlimit();
bump_memlock_rlimit()提高了内核内存限制,允许 BPF 子系统为 BPF 程序、映射等分配必要的资源。
/* Load and verify BPF application */
skel = minimal_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
minimal_bpf__open_and_load()使用自动生成的 BPF 框架,准备 BPF 程序并将其加载到内核中,并让 BPF 验证器对其进行检查。如果通过验证,则 附加到任何 BPF 挂钩上。
/* ensure BPF program only handles write() syscalls from our process */
skel->bss->my_pid = getpid();
首先,获取用户态进程的 PID 传给 BPF 代码,过滤不相关进程调用write()系统调用。
/* Attach tracepoint handler */
err = minimal_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("Successfully started!\n");
将内核中的 BPF 程序的handle_tp函数附加到相应的内核跟踪点。内核将开始在内核上下文中执行我们自定义的 BPF 代码,以响应以后每次的write()系统调用。
for (;;) {
/* trigger our BPF program */
fprintf(stderr, ".");
sleep(1);
}
将通过write()调用定期(每秒一次)生成系统调用fprintf(stderr, ...)调用。通过这种方式,可以监视内核的内部结构handle_tp以及状态如何随时间变化。
cleanup:
minimal_bpf__destroy(skel);
return -err;
}
minimal_bpf__destroy()将清理所有资源(包括内核和用户空间)。确保即使应用程序在没有清理的情况下崩溃,内核仍然会清理资源。
2.3 Makefile
Makefile 将代码编译为可执行文件。
# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
OUTPUT := .output
CLANG ?= clang
LLVM_STRIP ?= llvm-strip
BPFTOOL ?= $(abspath ../../tools/bpftool)
LIBBPF_SRC := $(abspath ../../libbpf/src)
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/' | sed 's/ppc64le/powerpc/' | sed 's/mips.*/mips/')
VMLINUX := ../../vmlinux/$(ARCH)/vmlinux.h
# Use our own libbpf API headers and Linux UAPI headers distributed with
# libbpf to avoid dependency on system-wide headers, which could be missing or
# outdated
INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX))
CFLAGS := -g -Wall
APPS = minimal bootstrap uprobe kprobe fentry
# Get Clang's default includes on this system. We'll explicitly add these dirs
# to the includes list when compiling with `-target bpf` because otherwise some
# architecture-specific dirs will be "missing" on some architectures/distros -
# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h,
# sys/cdefs.h etc. might be missing.
#
# Use '-idirafter': Don't interfere with include mechanics except where the
# build would have failed anyways.
CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - </dev/null 2>&1 \
| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')
ifeq ($(V),1)
Q =
msg =
else
Q = @
msg = @printf ' %-8s %s%s\n' \
"$(1)" \
"$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \
"$(if $(3), $(3))";
MAKEFLAGS += --no-print-directory
endif
.PHONY: all
all: $(APPS)
.PHONY: clean
clean:
$(call msg,CLEAN)
$(Q)rm -rf $(OUTPUT) $(APPS)
$(OUTPUT) $(OUTPUT)/libbpf:
$(call msg,MKDIR,[email protected])
$(Q)mkdir -p [email protected]
# Build libbpf
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
$(call msg,LIB,[email protected])
$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
OBJDIR=$(dir [email protected])/libbpf DESTDIR=$(dir [email protected]) \
INCLUDEDIR= LIBDIR= UAPIDIR= \
install
# Build BPF code
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT)
$(call msg,BPF,[email protected])
$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c $(filter %.c,$^) -o [email protected]
$(Q)$(LLVM_STRIP) -g [email protected] # strip useless DWARF info
# Generate BPF skeletons
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT)
$(call msg,GEN-SKEL,[email protected])
$(Q)$(BPFTOOL) gen skeleton $< > [email protected]
# Build user-space code
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
$(call msg,CC,[email protected])
$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o [email protected]
# Build application binary
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
$(call msg,BINARY,[email protected])
$(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o [email protected]
# delete failed targets
.DELETE_ON_ERROR:
# keep intermediate (.skel.h, .bpf.o, etc) targets
.SECONDARY:
INCLUDES := -I$(OUTPUT)
CFLAGS := -g -Wall
ARCH := $(shell uname -m | sed 's/x86_64/x86/')
INCLUDES:定义了一些在编译过程中使用的额外参数。默认情况下,所有中间文件都会写在src/.output/子目录下,这个目录被添加到 C 编译器路径中,用于加载BPF skel和 libbpf 头文件。
CFLAGS:用户态程序使用调试信息 ( -g) 进行编译,并且没有进行任何优化,方便调试。
ARCH:获取主机操作系统架构,将其传递到 BPF 代码编译步骤,与低级跟踪辅助宏(在 libbpf 中bpf_tracing.h)一起使用。
APPS = minimal bootstrap
应用程序的名称。为每个应用程序都定义了相应的 make 目标
$ make minimal
因此可以只构建某个APPS列表里相关程序。
# Build libbpf
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
$(call msg,LIB,[email protected])
$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
OBJDIR=$(dir [email protected])/libbpf DESTDIR=$(dir [email protected]) \
INCLUDEDIR= LIBDIR= UAPIDIR= \
install
构建过程首先,libbpf 被构建为静态库,其 API 头文件安装到.output。
# Build BPF code
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) vmlinux.h | $(OUTPUT)
$(call msg,BPF,[email protected])
$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) -c $(filter %.c,$^) -o [email protected]
$(Q)$(LLVM_STRIP) -g [email protected] # strip useless DWARF info
将 BPF C 代码 ( *.bpf.c) 构建到已编译的目标文件中。
# Generate BPF skeletons
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT)
$(call msg,GEN-SKEL,[email protected])
$(Q)$(BPFTOOL) gen skeleton $< > [email protected]
生成.bpf.o文件,bpftool用于生成相应的 BPF skel。
# Build user-space code
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
$(call msg,CC,[email protected])
$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o [email protected]
确保无论何时更新 BPF skel,应用程序的用户空间部分也会重新构建,因为它们需要在编译期间嵌入 BPF skel。
# Build application binary
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
$(call msg,BINARY,[email protected])
$(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o [email protected]
最后,仅使用用户态.o文件(以及libbpf.a静态库)生成最终的二进制文件。-lelf并且-lz是 libbpf 的依赖项,需要显式提供给编译器。
最终会得到独立200kb的用户空间二进制文件,它通过 BPF 框架嵌入编译后的 BPF 代码,并在其中静态链接 libbpf,因此不依赖于系统自带的libbpf 。
reference
https://www.cnblogs.com/davad/p/yiebpf-he-go-jing-yan-chu-tan.html
https://networkop.co.uk/post/2021-03-ebpf-intro/
https://cyral.com/blog/lessons-using-ebpf-accelerating-cloud-native/
http://arthurchiao.art/articles-zh/
https://nakryiko.com/posts/libbpf-bootstrap/
https://linux.cn/article-9507-1.html