概述

eBPF 全程是扩展伯克利包过滤(extended Berkeley Packet Filter),用于实现内核不支持的功能,通常情况下你只能通过 ePBF 读取系统的各种信息(主要是系统调用和网络包),少数情况下允许你修改信息(主要是网络包)。

eBPF 有多种开发工具,例如常见的:

Kernal Module

Kernel Module 同样可以在不修改内核代码的前提下扩展内核,但是,即使是资深的内核开发人员对待 Module 也是很小心,根本原因是:Module Code Crash 了,那么整个系统也会跟着崩溃,包括系统上运行的所有应用。

Crash 只是“系统安全”的其中一个考虑,此外,还有其他考虑,例如 Module 是不是可以有访问所有系统功能的权限,Module 的发布者是否是可信任的,会不会做一些恶意的操作之类的,这些都是扩展内核需要考量的。

Ubuntu 22 实操

系统参数:

  1. [root@liqiang.io]# uname -a
  2. Linux ebpf-dev 5.15.0-73-generic #80-Ubuntu SMP Mon May 15 15:18:26 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

设置环境

  1. [root@liqiang.io]# apt update
  2. [root@liqiang.io]# apt upgrade -y
  3. [root@liqiang.io]# apt install linux-tools-common linux-tools-5.15.0-73-generic
  4. [root@liqiang.io]# apt install -y bison flex build-essential cmake make clang llvm \
  5. git strace tar python3-distutils \
  6. libelf-dev libfl-dev libssl-dev libedit-dev zlib1g-dev libbpf-dev
  7. [root@liqiang.io]# apt install
  8. [root@liqiang.io]#
  9. [root@liqiang.io]#
  10. [root@liqiang.io]#
  11. [root@liqiang.io]#
  12. [root@liqiang.io]#
  13. [root@liqiang.io]#

环境到此就算设置完毕。

CentOS 8 实操

安装依赖

  1. [root@liqiang.io]# dnf groupinstall -y "Development Tools"
  2. [root@liqiang.io]# dnf install -y llvm-toolset llvm-devel bc
  3. [root@liqiang.io]# dnf --enablerepo=powertools install dwarves
  4. [root@liqiang.io]#
  5. [root@liqiang.io]#

install cargo

  1. [root@liqiang.io]# dnf -y install cargo

编译依赖工具

  1. [root@liqiang.io]# git clone https://github.com/libbpf/libbpf.git
  2. [root@liqiang.io]# cd libbpf/src
  3. [root@liqiang.io]# NO_PKG_CONFIG=1 make
  4. [root@liqiang.io]# BUILD_STATIC_ONLY=1 NO_PKG_CONFIG=1 PREFIX=/usr/local/bpf make install
  5. [root@liqiang.io]# cd ../../ && git clone https://github.com/libbpf/bpftool.git
  6. [root@liqiang.io]# cd bpftool && git submodule update --init --recursive
  7. [root@liqiang.io]# cd src && make
  8. [root@liqiang.io]# NO_PKG_CONFIG=1 make install

生成 skel 文件

bpftool gen skeleton 是一个命令,它的主要功能是根据指定的 BPF 二进制文件(即 .o 或 .bpf 文件)生成一个 C 语言头文件。这个头文件定义了一个用于操作 BPF 程序和 maps 的结构体,同时还包含一些用于加载和管理 BPF 对象的辅助函数。

在实际应用中,你可以在你的 C 或 C++ 程序中包含这个生成的头文件,然后使用其中的函数和数据结构来加载和操作 BPF 程序和 maps,而无需手动编写复杂的 BPF 系统调用代码。这样可以大大简化 BPF 程序的加载和使用。

  1. [root@liqiang.io]# CLANG ?= clang
  2. [root@liqiang.io]# ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/' | sed 's/ppc64le/powerpc/' | sed 's/mips.*/mips/')
  3. [root@liqiang.io]# BPFTOOL ?= /usr/local/sbin/bpftool
  4. [root@liqiang.io]#
  5. [root@liqiang.io]#
  6. [root@liqiang.io]# $(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c opensnoop.bpf.c # 生成 opensnoop.bfp.o
  7. [root@liqiang.io]# $(BPFTOOL) gen skeleton opensnoop.bpf.o > opensnoop.skel.h # 生成 opensnoop.skel.h

现在你就可以在你的 C 或 C++ 程序中包含 opensnoop.skel.h,并使用其中的函数和数据结构来加载和操作我们的 BPF 程序了。

install

确切说,vmlinux.h 是使用工具生成的代码文件。它包含了系统运行 Linux 内核源代码中使用的所有类型定义。当我们编译 Linux 内核时,会输出一个称作 vmlinux 的文件组件,其是一个 ELF 的二进制文件,包含了编译好的可启动内核。vmlinux 文件通常也会被打包在主要的 Linux 发行版中。

内核中的 bpftool 工具其中功能之一就是读取 vmlinux 文件并生成对应的 vmlinux.h 头文件。vmlinux.h 会包含运行内核中所使用的每一个类型定义,因此该文件的比较大。

生成 vmlinux.h 文件的命令如下:

  1. [root@liqiang.io]# bpftool btf dump file /usr/local/src/linux-6.1/vmlinux format c > vmlinux.h

包含该 vmlinux.h,就意味着我们的程序可以使用内核中使用的所有数据类型定义,因此 BPF 程序在读取相关的内存时,就可以映射成对应的类型结构按照字段进行读取。

例如,Linux 中的 task_struct 结构用于表示进程,如果 BPF 程序需要检查 task_struct 结构的值,那么首先就需要知道该结构的具体类型定义。

图 1:编译加载 eBPF 程序

由于 vmlinux.h 文件是由当前运行内核生成的,如果你试图将编译好的 eBPF 程序在另一台运行不同内核版本的机器上运行,可能会面临崩溃的窘境。这主要是因为在不同的版本中,对应数据类型的定义可能会在 Linux 源代码中发生变化。

但是,通过使用 libbpf 库提供的功能可以实现 “CO:RE”(一次编译,到处运行)。libbpf 库定义了部分宏(比如 BPF_CORE_READ),其可分析 eBPF 程序试图访问 vmlinux.h 中定义的类型中的哪些字段。如果访问的字段在当前内核定义的结构中发生了移动,宏 / 辅助函数会协助自动找到对应字段【译者注:对于可能消失的字段,也提供了对应的辅助函数 bpf_core_field_exists】。因此,我们可以使用当前内核中生成的 vmlinux.h 头文件来编译 eBPF 程序,然后在不同的内核上运行它【译者注:需要运行的内核也支持 BTF 内核编译选项】。

CO-RE

Many eBPF programs access kernel data structures, and an eBPF programmer would need to include relevant Linux header files so that their eBPF code can correctly locate fields within those data structures. However, the Linux kernel is under continuous development, which means internal data structures can change between different kernel versions. If you were to take an eBPF object file compiled on one machine1 and load it onto a machine with a different kernel version, there would be no guarantee that the data structures would be the same.

The CO-RE approach is a huge step forward in addressing this portability issue in an efficient way. It allows eBPF programs to include information about the data structure layouts they were compiled with, and it provides a mechanism for adjusting how fields are accessed if the data structure layout is different on the target machine where they run. Provided the program doesn’t want to access a field or data structure that simply doesn’t exist in the target machine’s kernel, the program is portable across different kernel versions.

关键概念

BTF

BTF,全称为 BPF Type Format,是一种用于描述程序二进制接口(Binary Interface)的格式,特别是用于描述 eBPF 程序和它们操作的数据结构。它被设计为简单、紧凑、易于解析,并且可以被用于多种用途。

BTF 可以帮助解决在编写和理解 eBPF 程序时遇到的一些挑战。特别地,BTF 可以提供以下帮助:

总的来说,BTF 是一种强大的工具,可以帮助 eBPF 程序员更好地编写、理解和维护他们的代码,同时提高代码的兼容性和可移植性。然而,如果在某个内核版本中,你想要访问的某个字段不存在,那么 BTF 并不能解决这个问题,因为 BTF 只能描述实际存在的数据结构。但是,BTF 可以帮助开发者在编译或运行时检测这个问题。例如,你可以在编译 eBPF 程序时检查 BTF 信息,看看你想要访问的字段是否存在。如果字段不存在,你可以选择编译一个不依赖该字段的版本的程序,或者给出一个错误消息,告诉用户这个程序不能在他们的内核版本上运行。

libbpf

eBPF 程序分为内核和用户空间两部分,一般分发的都是内核空间的部分,所以,用户空间部分是可以我们来自定义的。要实现 eBPF 内核空间的 CO-RE,那么就需要 libbpf 的支持,它可以根据 BTF 的信息重新整理 eBPF 生成的代码,并且在当前的内核版本中正确执行。

On the one hand, libbpf provides functions for loading eBPF programs and maps into the kernel. But it also plays an important role in portability: it leans on BTF information to adjust the eBPF code to compensate for any differences between the data structures present when it was compiled, and what’s on the destination machine.

How CO-RE achieves this by encoding type information into the compiled object file and using relocations to rewrite instructions as they are loaded into the kernel. You also had an introduction to writing code in C that uses libbpf: both the eBPF programs that run in the kernel and the user space programs that manage the lifecycle of those programs, based on auto-generated BPF skeleton code. In the next chapter you’ll learn how the kernel verifies that eBPF programs are safe to run.

问题

C 版本问题

Error: failed to load BTF from /usr/local/src/linux-6.1/vmlinux: No such file or directory

解决方式:重新编译内核,.config 带上以下配置

  1. [root@liqiang.io]# cat .config
  2. CONFIG_DEBUG_INFO_BTF=y
  3. CONFIG_DEBUG_INFO=y
  4. CONFIG_IKHEADERS=y
fatal error: ‘uapi/linux/bpf.h’ file not found
  1. [root@liqiang.io]# clang -O2 -target bpf -c hello_world_kern.c \
  2. -I/root/srcs/github.com/libbpf/libbpf/include
fatal error: ‘gnu/stubs-32.h’ file not found
  1. [root@liqiang.io]# sudo yum install glibc-devel.i686
运行错误: ‘openmp-ir-builder-optimistic-attributes’ registered more than once!

重新编译过 llvm,加上 -DENABLE_LLVM_SHARED=1 的选项。

  1. [root@liqiang.io]# cmake3 -DENABLE_LLVM_SHARED=1 -DPYTHON_CMD=python3 ..
运行错误:error: redefinition of ‘bpf_map_lookup_elem’ with a different typ
C 编译错误:error: ‘BPF_PROBE_ENTRY’ undeclared (first use in this function); did you mean ‘BPF_TRACE_FENTRY’?

Python 版本错误

fatal error: bcc/bcc_common.h: No such file or directory
  1. [root@liqiang.io]# pip3 install bcc
  2. [root@liqiang.io]# pip3 install numba
  3. [root@liqiang.io]# pip3 install pytest
运行错误: /virtual/main.c:4:10: fatal error: ‘bpf/bpf_helpers.h’ file not found

添加 cflags

  1. [root@liqiang.io]# cat test.py
  2. bpf_source = open("hello_world_kern.c").read()
  3. bpf = BPF(text=bpf_source, cflags=[
  4. "-I/usr/local/bpf/include",
  5. ])
  6. [root@liqiang.io]# cat test.go
  7. cflags := []string{
  8. "-I /usr/local/bpf/include",
  9. "-L /usr/local/bpf/lib64",
  10. "-lbpf",
  11. }
  12. // 加载 eBPF 程序
  13. bpfModule := bcc.NewModule(bpfProgram, cflags)

Go 版本错误

asm/types.h file not found
  1. [root@liqiang.io]# sudo ln -s /usr/include/x86_64-linux-gnu/asm /usr/include/asm
fatal error: bcc/bcc_common.h: No such file or directory
  1. [root@liqiang.io]# dnf install -y epel-release
  2. [root@liqiang.io]# dnf install -y git cmake3 make gcc gcc-c++ python3 python3-devel llvm llvm-devel clang clang-devel kernel-devel kernel-headers elfutils-libelf-devel
  3. [root@liqiang.io]#
  4. [root@liqiang.io]#
  5. [root@liqiang.io]#
  6. [root@liqiang.io]# git clone --branch v0.24.0 https://github.com/iovisor/bcc.git
  7. [root@liqiang.io]# mkdir bcc/build; cd bcc/build
  8. [root@liqiang.io]# cmake3 -DENABLE_LLVM_SHARED=1 -DPYTHON_CMD=python3 ..
  9. [root@liqiang.io]# make && make install
  10. [root@liqiang.io]# echo 'export PATH=$PATH:/usr/local/share/bcc/tools' | sudo tee -a /etc/profile
  11. [root@liqiang.io]# source /etc/profile
  12. [root@liqiang.io]#
not enough arguments in call to (_C2func_bcc_func_load)
  1. [root@liqiang.io]# go list -m github.com/iovisor/gobpf@master
  2. github.com/iovisor/gobpf v0.2.1-0.20221005153822-16120a1bf4d4
  3. [root@liqiang.io]# go tail go.mod
  4. require github.com/iovisor/gobpf v0.2.1-0.20221005153822-16120a1bf4d4
  5. [root@liqiang.io]#

Ref