最常见方法是使用C语言子集编写BPF程序,之后使用LLVM编译器进行编译。LLVM是一种通用编译器,可以编译成不同类型的字节码。针对BPF程序,LLVM能够编译出加载到内核中执行的汇编代码。本书不会过多地介绍BPF汇编。经过慎重考虑,我们决定本书主要介绍在具体情景下如何使用BPF,关于BPF汇编,你可以在网上轻松地找到一些参考或者通过BPF手册得到帮助。不过,之后的章节会有BPF汇编代码的简短示例,你会发现在某些情景下使用汇编比C语言更合适,例如,使用Seccomp过滤器控制内核的系统调用。我们将在第8章中详细讨论Seccomp。
BPF程序编译后,内核通过bpf系统调用将程序字节码加载到BPF虚拟机中。除加载程序外,bpf系统调用还可用于其他操作,我们将在后续章节中看到更多的示例。内核还提供了一些工具(帮助函数)协助加载BPF程序。在第一个代码示例中,我们将使用这些帮助函数来实现BPF的“Hello World”:
#include <linux/bpf.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";
第一个BPF程序中包括一些有用的概念。我们使用SEC属性告知BPF虚拟机何时运行此程序。在本例中,当检测到execve系统调用跟踪点被执行时,BPF程序将运行。跟踪点是内核二进制代码中的静态标记,允许开发人员注入代码来检查内核的执行。我们将在第4章中详细讨论跟踪点。这里,你只需知道execve是执行其他程序的指令。当内核检测到execve执行时,BPF程序被执行,我们会看到消息输出:Hello,BPF World!。
在示例的最后,我们还需要指定程序许可证。因为Linux内核采用GPL许可证,所以它只能加载GPL许可证的程序。如果将程序设置为其他许可证,内核将拒绝加载该程序。我们使用函数bpf_trace_printk在内核跟踪日志中打印消息,你可以在文件/sys/kernel/debug/tracing/trace_pipe中查看。
我们可以使用clang将第一个程序编译成内核可加载的ELF(Executable and Linkable Format)格式的二进制文件。我们将第一个BPF程序保存为bpf_program.c,使用下面的命令进行编译:
clang -O2 -target bpf -c bpf_program.c -o bpf_program.o
你可以在GitHub仓库中找到本书的代码示例及编译程序的脚本( https://oreil.ly/lbpf-repo ),因此你不必记住这个clang命令。
现在,我们已经编译了第一个BPF程序,我们需要将它加载到内核中运行。如前所述,我们使用内核提供的特定帮助函数,该帮助函数会对编译和加载程序按模板抽象进行处理。这个帮助函数叫load_bpf_file,它将获取一个二进制文件将它加载到内核中。GitHub上包括书中( https://oreil.ly/lbpf-repo )所有的示例,示例在文件bpf_load.h中,如下所示:
#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) { 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虚拟机中卸载。在后续的章节中,我们将讨论如何使BPF程序持久化,BPF程序甚至可以在加载器终止后继续运行,但现在,我们不想引入太多概念。请牢记这一重要概念,因为在许多情况下,你将在后台运行BPF程序,从系统中收集数据,不管其他进程是否正在运行。
至此,你已经了解了BPF程序的基本架构。下面将深入探究BPF程序类型,我们使用这些BPF程序类型来访问Linux内核中不同的子系统。