Linux | libbpf-bootstrap编写BPF程序

admin 2022年8月31日22:22:05评论1,041 views字数 7930阅读26分26秒阅读模式

        编写eBPF程序,通常通过Cilium、bcc或bpftrace等项目间接使用,这些项目提供eBPF抽象,不需要直接编写程序,而是使用预定义功能和帮助函数实现eBPF相关功能。相关框架利用bpf系统调用将eBPF程序加载到Linux内核中。


Linux | libbpf-bootstrap编写BPF程序


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) :

Linux | libbpf-bootstrap编写BPF程序

#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)

Linux | libbpf-bootstrap编写BPF程序

#include "minimal.skel.h"

    minimal.skel.h头文件包括 BPF 代码的 BPF 框架minimal.bpf.c。它由 bpftool 在 Makefile 中自动生成的,它还将编译后的 BPF 目标代码的内容嵌入头文件中来简化 BPF 代码部署工作,该头文件包含在用户态代码中。无需在应用程序二进制文件中部署额外的文件,只需包含标头就可以了。

    src/.output/minimal.skel.h 在成功make后生成。

Linux | libbpf-bootstrap编写BPF程序 

    bpf_object *ob j结构体可以传递maps、progs和links给 libbpf API 函数。

Linux | libbpf-bootstrap编写BPF程序

    比如,供对 BPF 映射和 BPF 代码中定义的程序(例如,handle_tp BPF 程序)的直接访问。

    Skeleton 还可以选择具有允许从用户空间直接(不需要额外的系统调用)访问 BPF 全局变量的bss、data部分。

    用户空间(minimum.c)main函数:


Linux | libbpf-bootstrap编写BPF程序

/* 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 skeletonn");    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 skeletonn");    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 := .outputCLANG ?= clangLLVM_STRIP ?= llvm-stripBPFTOOL ?= $(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# outdatedINCLUDES := -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%sn' "$(1)" "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" "$(if $(3), $(3))"; MAKEFLAGS += --no-print-directoryendif
.PHONY: allall: $(APPS)
.PHONY: cleanclean: $(call msg,CLEAN) $(Q)rm -rf $(OUTPUT) $(APPS)
$(OUTPUT) $(OUTPUT)/libbpf: $(call msg,MKDIR,$@) $(Q)mkdir -p $@
# Build libbpf$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf $(call msg,LIB,$@) $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) INCLUDEDIR= LIBDIR= UAPIDIR= install
# Build BPF code$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(call msg,BPF,$@) $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c $(filter %.c,$^) -o $@ $(Q)$(LLVM_STRIP) -g $@ # strip useless DWARF info
# Generate BPF skeletons$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(call msg,GEN-SKEL,$@) $(Q)$(BPFTOOL) gen skeleton $< > $@
# Build user-space code$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) $(call msg,CC,$@) $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
# Build application binary$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) $(call msg,BINARY,$@) $(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o $@
# delete failed targets.DELETE_ON_ERROR:
# keep intermediate (.skel.h, .bpf.o, etc) targets.SECONDARY:
INCLUDES := -I$(OUTPUT)CFLAGS := -g -WallARCH := $(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,$@)    $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1                          OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@)                          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,$@)    $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) -c $(filter %.c,$^) -o $@    $(Q)$(LLVM_STRIP) -g $@ # strip useless DWARF info

    将 BPF C 代码 ( *.bpf.c) 构建到已编译的目标文件中。

# Generate BPF skeletons$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT)    $(call msg,GEN-SKEL,$@)    $(Q)$(BPFTOOL) gen skeleton $< > $@

    生成.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,$@) $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@

    确保无论何时更新 BPF  skel,应用程序的用户空间部分也会重新构建,因为它们需要在编译期间嵌入 BPF  skel

# Build application binary$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)    $(call msg,BINARY,$@)    $(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o $@

    最后,仅使用用户态.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

原文始发于微信公众号(TahirSec):Linux | libbpf-bootstrap编写BPF程序

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年8月31日22:22:05
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Linux | libbpf-bootstrap编写BPF程序https://cn-sec.com/archives/1267522.html

发表评论

匿名网友 填写信息