《Linux内核观测技术BPF》
《Linux内核观测技术BPF》
目录
运行BPF程序
LLVM 编译器
LLVM编译器
最常见方住是使用 C 语言子集编写 BPF 程序,之后使用 LLVM 编译器进行编译。
LLVM 编译器 是一种通用编译器,可以编译成不同类型的字节码 。
字节码
针对 BPF 程序, LLVM 能够编译出加载到内核中执行的汇编代码。本书不会过多地介绍 BPF 汇编。经过慎重考虑,我们决定本书主要介绍在具体情景下如何使用 BDF,关于 BPF 汇编,你可以在网上轻松地找到-些参考或者通过 BPF 手册得到帮助。不过,之后的章节会有 BPF 汇编代码的简短示例,你会发现在某些情景下使用汇编比 C 语言更合适,例如,使用 Seccomp 过滤器控制内核的系统调用。我们将在第 8 章中详细讨论 Seccompo。
加载字节码
BPF 程序编译后,内核通过 bpf 系统调用将程序字节码加载到 BPF 虚拟机中 。 除加载程序外, bpf 系统调用还可用于其他操作,我们将在后续章节中看到更多的示例。内核还提供了一些工具 (帮助函数) 协助加载 BPF 程序。
许可证(GPL)
在下面示例的最后,我们还需要指定程序许可证。因为 Linux 内核采用 GPL 许可证,所以它只能加载 GPL 许可证的程序。如果将程序设置为其他许可证,内核将拒绝加载该程序。
我们使用函数 bpf_trace_printk 在内核跟踪日志中打印消息,你可以在文件 /syslkernel/debug/tracing/traceyipe 中查看。
第一个代码
代码
在第一个代码示例中,我们将使用这些帮助函数来实现 BPF 的" Hello World":
#include <linux/bpf.h>
#define SEC(NAME) __attribute__((section(NAME) , used))
SEC("tracepoint/syscalls/sys_enter_execve") // 使用SEC属性告知BPF虚拟机何时运行此程序。当检测到 execve 系统调用跟踪点被执行时,BPF 程序将运行,我们会看到消息输出 “Hello,BPF World!”。
int bpf_prog(void *ctx) {
char msg[] "Hello, BPF World!";
bpf_trace_printk(msg, sizeof(msg));
return 0;
}
char _license[] SEC("license") "GPL";
跟踪点是内核二进制代码中的静态标记,允许开发人员注入代码来检查内核的执行。我们将在第 4 章中详细讨论跟踪点。
编译
我们可以使用 clang 将第 一 个程序编译成内核可加载的 ELF (Executable and Linkable Format) 格式的二进制文件。我们将第一个 BPF 程序保存为 bpf_program.c ,使用下面的命令进行编译:
clang -O2 -target bpf -c bpf_program.c -o bpf_program.o
运行
现在,我们已经编译了第一个 BPF 程序,我们需要将它加载到内核中运行 。如前所述,我们使用内核提供的特定帮助函数,该帮助函数会对编译和加载程序按模板抽象进行处理。这个帮助函数叫 load_bp_file
,它将获取一 个二进制文件将它加载到内核中。 如下所示:
#include <stdio.h >
#include <uapi/linux/bpf.h >
#include "bpf_load.h"
int main(int argc , char **argv) {
if (load_bpf_file("hello_world_kern.o") != 0) { // load_bpf_file 帮助函数
printf("The kernel didn't load the BPF program\n");
return -1;
}
read_trace_pipe();
return 0;
}
我们可以使用脚本编译和链接该程序生成 ELF 二进制文件 。 这里,我们不需要指定编译后的目标文件,因为该程序不会加载到 BPF 虚拟机中。编译程序需要一些依赖库,编写如下脚本会更容易将外部依赖库放在一起:
TOOLS=../../../tools
INCLUDE=../../../libbpf/include
HEADERS=../../../libbpf/src
clang -o loader -l elf \
-I${INCLUDE} \
-I${HEADERS} \
-I${TOOLS} \
${TOOLS}/bpf_load.c \
loader.c
如果你要运行该程序,可以使用 sudo 执行此二进制文件 sudo ./loader。
sudo 是一个 Linux 命令,为你提供计算机的 root 特权。如果你不使用 sudo 运行该程序,将会返回错误消息,因为对于大多数 BPF 程序而言,只能由root 特权用户加载到内核中。
运行该程序,即使计算机不进行任何操作,几秒钟后你也将会看到 "Hello ,BPF World !"。这是因为计算机是一个井行的系统,其他程序在后台执行,可能正在执行其他程序。
程序停止后,消息将不在终端上显示。 一旦程序终止,加载的 BPF 程序将从BPF 虚拟机中卸载。在后续的章节中,我们将讨论如何使 B PF 程序持久化,BPF 程序甚至可以在加载器终止后继续运行,但现在,我们不想引人太多概念。请牢记这一重要概念,因为在许多情况下,你将在后台运行 BPF 程序,从系统中收集数据,不管其他进程是否正在运行。
BPF程序类型
虽然 BPF 程序没有明确的分类,但本节根据其主要目的,可以将 BPF 程序分为两类:
- 跟踪
- 你能编写程序来更好地了解系统正在发生什么。这类程序提供了系统行为及系统硬件的直接信息。
例如:- 访问特定程序的内存区域,从运行进程中提取执行跟踪信息
- 直接访问为每个特定进程分配的资源,包括文件描述符、 CPU 和内存
- 你能编写程序来更好地了解系统正在发生什么。这类程序提供了系统行为及系统硬件的直接信息。
- 网络
- 这类程序可以检测和控制系统的网络流量。可以对网络接口的数据包进行过滤,甚至可以完全拒绝数据包。我们可以使用不同的程序类型,将程序附加到内核网络处理的不同阶段上,这样做各有利弊。
例如:- 将 BPF 程序附加到网络驱动程序接收数据包的网络事件上,此时,由于内核没有提供足够的信息,程序也只能访问较少的数据包信息。另 一方面,你可以将 BPF 程序附加到数据包传递给用户空间的网络事件。在这种情况下,你可以获得更多的数据包信息,这将有助于你做出更明智的决策,但是完整地处理这些数据包信息需要付出更高的代价。
- 这类程序可以检测和控制系统的网络流量。可以对网络接口的数据包进行过滤,甚至可以完全拒绝数据包。我们可以使用不同的程序类型,将程序附加到内核网络处理的不同阶段上,这样做各有利弊。
套接字过滤器程序
(我们将在第 6 章中详细介绍套接宇过滤程序和其他网络程序)
BPF PROG TYPE SOCKET FILTER
类型是添加到 Linux 内核的第一个程序类型。套接字过滤器程序会附加到原始套接字上,用于访问所有套接字处理的数据包。套接宇过滤器程序只能用于对套接字的观测,不允许修改数据包内容或更改其目的地。 BPF 程序会接收与网络协议桔信息相关的元数据,例如,发送数据包的协议类型。
kprobe 程序
kprobe 是动态附加到内核调用点的函数,我们将在第 4 章介绍跟踪时对kprobe 进行详细介绍。 BPF kprobe 程序类型允许使用 BPF 程序作为 kprobe的处理程序。程序类型被定义为 BPF_PROG_TYPE_KPROBE o BPF 虚拟机确保 kprobe 程序总是可以安全运行 , 这是传统 kprobe 模块的优势。但需要注意,在内核中 kprobe 被认为是不稳定的入口点,因此你需要确定 kprobe BPF 程序是否与正在使用的特定内核版本兼容。
编写附加到 kprobe 模块上的 BPF 程序,我们需要确定 BPF 程序是在函数调用的第一条指令执行还是函数调用完成时执行。我们需要在 BPF 程序的 SEC头部声明行为。例如,如果你想在内核 exec 系统调用前检查参数,则需要在系统调用开始时附加 BPF 程序。
这里,你需要在 BPF 程序设置 SEC 头部: SEC("kprobe/sys_exec")
。
如果你想检查 exec 系统调用的返回值,需要在 BPF 程序设置 SEC 头部 SEC("kretprobe/sys_exec")