vmlinux.h
在eBPF(Extended Berkeley Packet Filter)编程中,vmlinux.h
文件扮演了一个非常重要的角色,特别是在创建针对Linux内核的eBPF程序时。
定义
vmlinux.h
是一个头文件,它包含Linux内核的类型和函数定义。这个文件对于需要访问内核数据结构的eBPF程序来说非常关键,因为它提供了与内核源代码一致的数据结构和API定义;- 在最新的Linux内核中,
vmlinux.h
通常由BPF程序使用,以保证在编写程序时能够正确解析和操作内核数据结构。
作用
- 保证数据结构一致性:在编写eBPF程序时,访问内核中的数据结构(如任务结构体、套接字结构体等)是非常常见的。
vmlinux.h
保证了eBPF程序在编译时能够获得精确的结构定义,避免因为结构体布局不一致导致的错误; - 提供API访问:除了数据结构,
vmlinux.h
还可能包含对某些内核API的定义,这些API对于eBPF程序可能是可访问的,使得eBPF程序能够利用内核现有的功能和逻辑。
首先,你需要获得vmlinux.h这个头文件,这个文件的获取方法比较多。利用bpftool可以获得:
yum update
yum install bpftool
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
Bpftool是管理和调试BPF(Berkeley Packet Filter)程序的工具,来生成vmlinux.h
文件。vmlinux.h
文件包含了Linux内核的类型定义,这对于编写eBPF程序至关重要。
-
bpftool
:
用于Linux的BPF子系统的工具。它支持多种操作,包括对BPF程序和map的管理,以及对BPF Type Format(BTF)信息的提取; -
btf
:
这是bpftool
的一个子命令,专门用于处理与BTF相关的操作。BTF是一种用于描述eBPF程序中数据结构的格式,允许eBPF程序以可移植的方式访问内核数据结构; -
dump
:
这个选项指定要输出BTF数据; -
file /sys/kernel/btf/vmlinux
:
指定要从中提取BTF数据的文件。vmlinux
是内核提供的包含完整内核BTF信息的文件; -
format c
:
指定输出格式为C语言源代码,这使得输出可以直接用作C头文件; -
> vmlinux.h
:
将命令的输出重定向到vmlinux.h
文件中。这样,生成的头文件就包含了内核的所有数据结构定义,可以在编写eBPF程序时使用。
作用
-
生成C格式的内核类型定义
:
通过这条命令,你可以生成一个包含所有内核数据类型的C头文件(vmlinux.h
)。这对于开发依赖于精确内核数据结构布局的eBPF程序非常有用; -
便于eBPF开发
:
有了这个文件,开发者可以确保他们的eBPF程序使用的数据结构与当前运行的内核版本完全一致,从而避免因版本不匹配引起的运行时错误。
BTF(BPF Type Format)是一种元数据格式。/sys/kernel/btf/vmlinux就是BTF格式的文件。
root@localhost A-Ops-Working]# file /sys/kernel/btf/vmlinux
/sys/kernel/btf/vmlinux: data
*.bpf.c内核态程序编写
这个是用C语言编写但是运行于内核态的bpf程序。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
int my_pid = 0;
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
int pid = bpf_get_current_pid_tgid() >> 32;
if (pid != my_pid)
return 0;
bpf_printk("BPF triggered from PID %d.\\n", pid);
return 0;
}
eBPF程序提供的功能(部分全局变量和函数)都需要通过SEC()
(自来bpf_helpers.h
)宏自定义section名称。当然这只是一个约定,但如果遵循libbpf的section名称,会有更好开发体验,如下:
tp/<category>/<name>
用于Tracepoints:Tracepoints是内核中预定义的静态跟踪点,允许开发者在特定的代码路径上附加eBPF程序来观察和分析内核运行时的行为。使用Tracepoints,开发者可以在不修改内核源代码的情况下,捕捉到内核中重要事件的发生(如调度事件、文件系统操作等);kprobe/<func_name>
用于Kprobe:Kprobes允许开发者动态地在几乎任何内核函数的开始处附加eBPF程序,是一种动态跟踪技术。Kprobes主要用于开发和调试,可以用来检测和记录函数调用及其参数值,非常适合于调试或分析内核的行为;kretprobe/<func_name>
用于Kretprobe:Kretprobes类似于Kprobes,但它们附加在函数的返回点。这允许开发者捕获函数的返回值和状态。Kretprobes 适用于那些需要知道函数执行后状态或结果的场景,如函数返回值检查,或评估函数执行结果的情况;raw_tp/<name>
用于原始Tracepoint:Raw Tracepoints是比普通Tracepoints更低级的钩子,提供了更少的抽象和更直接的访问内核行为的能力。Raw Tracepoints主要用于性能分析,它们提供了对事件的更直接的访问,可能会更快,但使用它们需要更深入的内核知识。
例如,如果不声明:
char LICENSE[] SEC("license") = "Dual BSD/GPL";
就会报以下错误:
cannot call GPL-restricted function from non-GPL compatible program
这意味着,这不是简单的约定!但是my_pid等全局变量并不必须使用SEC,按照C,它在bss段。
bpf_get_current_pid_tgid返回低32位的PID(内核的PID视图,在用户空间中通常表示为线程 ID)和高32位的线程组ID(用户空间通常认为PID)。通过直接将其设置为u32,我们丢弃了高32位;也可以通过右移32位直接获得用户空间的PID。bpf_printk会将字符串输出到以下路径:
/sys/kernel/debug/tracing/trace_pipe
编译bpf内核态程序
clang -g -O2 -target bpf -D__TARGET_ARCH_x86_64 -I/usr/include/x86_64-linux-gnu -I. -c minimal.bpf.c -o minimal.bpf.o
使用clang
编译器来编译一个eBPF(Extended Berkeley Packet Filter)程序,从C源代码文件(这里的 minimal.bpf.c
)生成BPF字节码文件(这里的minimal.bpf.o
)。每个部分的参数都有特定的意义和作用。下面是对这条命令的各部分参数的详细解释:
clang:
LLVM项目的C/C++/Objective-C编译器,广泛用于现代软件开发,包括eBPF程序;-g
:
这个选项告诉clang
在编译时包含调试信息;-O2
:指定编译器的优化级别。-O2
是一个中等程度的优化,提供了编译速度与代码执行性能之间的平衡(-O2选项也必须要有,不然会加载bpf程序失败;经测试,-O/-O1选项也可以);-target bpf
:
告诉clang
编译器生成的目标是BPF虚拟机。这是必需的,因为eBPF程序需要在特定于BPF的虚拟机上运行,而不是常规的硬件架构;-D__TARGET_ARCH_x86_64
:
这是一个预处理指令,定义了一个宏__TARGET_ARCH_x86_64;
-I/usr/include/x86_64-linux-gnu
和-I
:-I
参数用于指定clang
在寻找包含文件(header files)时应考虑的目录。-I/usr/include/x86_64-linux-gnu
指定了标准库头文件的路径,这对于包含在许多Linux发行版上的通用库非常有用。-I.
则告诉编译器在当前目录下查找包含文件;-c
:
这个选项指示clang
只编译源文件,但不进行链接。这是编译eBPF程序的常见做法,因为BPF程序通常不需要链接成一个完整的可执行文件,而是生成一个可由内核加载的对象文件;minimal.bpf.c
:
这是要编译的源代码文件名;-o minimal.bpf.o
:
指定输出文件名。编译后的结果将保存在这个文件中。
-g选项必须有以保留缺省调试信息,否则无法通过skel.h文件直接访问bss段的my_pid变量。
生成*.skel.h头文件
bpftool gen skeleton minimal.bpf.o > minimal.skel.h
使用bpftool
成一个称为"skeleton"的头文件。Skeleton文件是一种在eBPF程序与用户空间应用程序之间创建接口的方法。
-
gen skeleton
:gen skeleton
是bpftool
的一个子命令,用于从一个BPF对象文件生成C语言的skeleton头文件。这个过程将BPF程序信息转换为可以在用户空间程序中方便使用的格式; -
minimal.bpf.o
:
是BPF程序编译后的对象文件,其中包含了要加载到内核的BPF字节码及其相关的BPF映射定义; -
minimal.skel.h
:
将bpftool
的输出重定向到文件minimal.skel.h
。生成的头文件包含了嵌入的BPF程序代码和API,用于初始化、加载和附加BPF程序到内核,以及管理与之关联的BPF map。
生成的skeleton文件提供了一组函数和结构体,这些可以直接在用户空间程序中使用,极大简化了BPF程序的加载和管理流程。用户不需要直接处理BPF系统调用或复杂的加载逻辑,而可以通过简单的函数调用来操作BPF程序和map。Skeleton文件创建了一个标准化的接口,使得用户空间程序与BPF程序之间的交互更加清晰和可靠。
*.c用户态程序编写
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "minimal.skel.h"
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
static void bump_memlock_rlimit(void)
{
struct rlimit rlim_new = {
.rlim_cur = RLIM_INFINITY,
.rlim_max = RLIM_INFINITY,
};
if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) {
fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\\n");
exit(1);
}
}
int main(int argc, char **argv)
{
struct minimal_bpf *skel;
int err;
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
/* Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything */
bump_memlock_rlimit();
/* Open BPF application */
skel = minimal_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\\n");
return 1;
}
/* ensure BPF program only handles write() syscalls from our process */
skel->bss->my_pid = getpid();
/* Load & verify BPF programs */
err = minimal_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\\n");
goto cleanup;
}
/* Attach tracepoint handler */
err = minimal_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\\n");
goto cleanup;
}
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe`"
"to see output of the BPF programs.\\n");
for (;;) {
/* trigger our BPF program */
fprintf(stderr, ".");
sleep(1);
}
cleanup:
minimal_bpf__destroy(skel);
return -err;
}
libbpf_set_print设置libbpf的错误和调试信息回掉打印函数,最后一个参数为变参数量列表va_list。第一个参数为消息类型,有以下:
LIBBPF API
[cpp:enumerator]: libbpf_print_level::LIBBPF_DEBUG
[cpp:enumerator]: libbpf_print_level::LIBBPF_INFO
[cpp:enumerator]: libbpf_print_level::LIBBPF_WARN
RLIMIT_MEMLOCK定义一个非特权进程可以锁定多少内存。 锁定一个内存区域可以防止它被交换。在这里增加RLIMIT_MEMLOCK的限制以允许BPF子系统做任何事情!
BPF一般分为4个阶段:open, load, attachment和destroy。
编译bpf用户态程序
clang -Wall -I. -c minimal.c -o minimal.o
clang -Wall minimal.o -L/usr/lib64 -lbpf -lelf -lz -o minimal
编译源文件
-
-Wall
:
启用所有警告信息。这个编译选项非常有用,因为它可以帮助开发者在代码中发现可能的问题; -
-I
:
指定包含文件搜索目录。这里的.
表示当前目录,意味着clang
会在当前目录中查找任何#include
指令引用的头文件; -
-c
:
只编译源代码,不进行链接。这会生成一个对象文件(.o
文件),该文件包含了源代码编译后的机器代码,但尚未与其他对象文件或库文件进行链接; -
minimal.c
:
指定要编译的源代码文件; -
-o minimal.o
:
指定输出文件名。这里,编译后的输出将被保存为minimal.o
。
可执行文件
-
-L/usr/lib64
:
指定库文件搜索目录。这里指定/usr/lib64
,告诉链接器在这个目录下搜索需要链接的库; -
-lbpf -lelf -lz
:
指定链接器需要链接的库。-lbpf
表示链接 BPF 库,-lelf
用于链接 ELF(可扩展链接格式)处理库,-lz
用于链接 zlib,这是一个压缩库; -
-o minimal
:
指定最终生成的可执行文件的名称。
由于openEuler不支持静态链接libbpf,因此只能使用动态链接,并且需要制定链接库路径,否则ld会默认找不到libbpf.so动态链接库。
运行bpf用户态程序
[root@localhost L-Bpf]# ./minimal
libbpf: loading object 'minimal_bpf' from buffer
libbpf: elf: section(2) .text, size 16, link 0, flags 6, type=1
libbpf: sec '.text': found program 'main' at insn offset 0 (0 bytes), code size 2 insns (16 bytes)
libbpf: elf: section(3) tp/syscalls/sys_enter_write, size 192, link 0, flags 6, type=1
libbpf: sec 'tp/syscalls/sys_enter_write': found program 'handle_tp' at insn offset 0 (0 bytes), code size 24 insns (192 bytes)
libbpf: elf: section(4) .reltp/syscalls/sys_enter_write, size 16, link 25, flags 0, type=9
libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1
libbpf: license of minimal_bpf is Dual BSD/GPL
libbpf: elf: section(6) .bss, size 4, link 0, flags 3, type=8
libbpf: elf: section(7) .rodata.str1.1, size 28, link 0, flags 32, type=1
libbpf: elf: skipping unrecognized data section(7) .rodata.str1.1
libbpf: elf: section(16) .BTF, size 535, link 0, flags 0, type=1
libbpf: elf: section(18) .BTF.ext, size 200, link 0, flags 0, type=1
libbpf: elf: section(25) .symtab, size 360, link 1, flags 0, type=2
libbpf: looking for externs among 15 symbols...
libbpf: collected 0 externs total
libbpf: map 'minimal_.bss' (global data): at sec_idx 6, offset 0, flags 400.
libbpf: map 0 is "minimal_.bss"
libbpf: sec '.reltp/syscalls/sys_enter_write': collecting relocation for section(3) 'tp/syscalls/sys_enter_write'
libbpf: sec '.reltp/syscalls/sys_enter_write': relo #0: insn #2 against 'my_pid'
libbpf: prog 'handle_tp': found data map 0 (minimal_.bss, sec 6, off 0) for insn 2
libbpf: map 'minimal_.bss': created successfully, fd=4
Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe`to see output of the BPF programs.
...
打印输出结果
[root@localhost L-Bpf]# cat /sys/kernel/debug/tracing/trace_pipe
minimal-4840 [003] d... 11429.133261: bpf_trace_printk: BPF triggered from PID 4840.
minimal-4840 [003] d... 11429.133377: bpf_trace_printk: BPF triggered from PID 4840.
minimal-4840 [003] d... 11430.133721: bpf_trace_printk: BPF triggered from PID 4840.