使用LIBBPF开发BPF程序

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)。每个部分的参数都有特定的意义和作用。下面是对这条命令的各部分参数的详细解释:

  • clangLLVM项目的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 skeletongen skeletonbpftool的一个子命令,用于从一个BPF对象文件生成C语言的skeleton头文件。这个过程将BPF程序信息转换为可以在用户空间程序中方便使用的格式;

  • minimal.bpf.o是BPF程序编译后的对象文件,其中包含了要加载到内核的BPF字节码及其相关的BPF映射定义;

  • minimal.skel.hbpftool的输出重定向到文件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.

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LIHAORAN99

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值