目录

代码分析

BPF 程序部分

功能说明

头文件引入说明

SEC 关键字说明

bpf_get_current_pid_tgid() >> 32

用户程序部分

功能说明

头文件引入介绍

libbpf_set_print

minimal_bpf__open

skel->bss->my_pid = getpid();

minimal_bpf__load(skel);

minimal_bpf__attach(skel);

minimal_bpf__destroy(skel);

执行效果


这个样例也是最简单的样例,会监测write 系统调用,当用户程序调用 "sys_write" 系统调用时,输出一条内核日志,输出自己是哪一个进程trigger 的

代码分析

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;
}
功能说明

在上述代码中,int pid = bpf_get_current_pid_tgid() >> 32; 将当前进程的 PID 存储在 pid 变量中,以便后续的处理。该代码位于 handle_tp 函数中,用于在 "sys_enter_write" 系统调用的 Tracepoint 中获取当前进程的 PID,并与另一个全局变量 my_pid 进行比较。如果当前进程的 PID 不等于 my_pid,则该函数将直接返回 0,否则将打印一条调试信息并返回 0。

头文件引入说明
  1. <linux/bpf.h>:该头文件包含了 Linux 内核中与 BPF 相关的定义和数据结构。例如,它定义了 BPF 程序中常用的指令、寄存器、内存布局等等。开发人员可以使用这些定义和数据结构来编写 BPF 程序。
  2. <bpf/bpf_helpers.h>:该头文件包含了一些辅助函数和宏,用于在 BPF 程序中进行常见操作。例如,它定义了一个名为 bpf_map_lookup_elem() 的函数,用于在 BPF Map 中查找元素;还定义了一个名为 SEC() 的宏,用于指定 BPF 程序的安全属性。

bpf_get_current_pid_tgid():该函数用于获取当前进程的 PID 和 TGID(线程组 ID)。该函数定义在 <bpf/bpf_helpers.h> 头文件中,用于在 BPF 程序中获取当前进程的标识符。

bpf_printk():该函数用于在内核日志中打印一条调试信息。该函数定义在 <bpf/bpf_helpers.h> 头文件中,用于在 BPF 程序中输出调试信息。

SEC():该关键字用于指定一个 BPF 程序的安全属性。该关键字定义在 <bpf/bpf_helpers.h> 头文件中,用于在 BPF 程序中指定程序的安全属性。

SEC 关键字说明

SEC 是 BPF(Berkeley Packet Filter)程序中的一个特殊关键字,用于指定一个程序的安全属性。在上述代码中,SEC 用于指定两个部分的安全属性,分别为 LICENSE 变量和 handle_tp 函数。

  1. LICENSE 变量的安全属性:char LICENSE[] SEC("license") = "Dual BSD/GPL"; 这行代码将 LICENSE 字符串常量指定为 "license" 类型的 SEC。这意味着在加载和验证这个 BPF 程序时,内核将会检查该程序中所有标记为 "license" 类型的 SEC,并验证它们的安全性。在本例中,SEC("license") 指定了 LICENSE 常量的安全属性为 "license"。
  2. handle_tp 函数的安全属性:SEC("tp/syscalls/sys_enter_write") 这行代码将 handle_tp 函数指定为 "tp/syscalls/sys_enter_write" 类型的 SEC。这意味着该函数将会在与 "tp/syscalls/sys_enter_write" 相关的 hook 点处执行,并具有相应的安全属性。在本例中,handle_tp 函数被指定为在 "sys_enter_write" 系统调用的 Tracepoint 中执行。

SEC 的作用可以在 BPF 程序的加载和验证过程中体现。在加载 BPF 程序时,内核需要验证程序的安全性,以确保程序不会对系统造成损害。SEC 可以用于指定程序的安全属性,以供内核进行验证。例如,如果一个程序需要在内核中执行敏感操作,那么开发人员可以使用 SEC 来指定该程序的安全属性为 "trusted",以确保程序能够被正确加载和验证。

在本例中,SEC("tp/syscalls/sys_enter_write") 指定了 handle_tp 函数的安全属性为 "tp/syscalls/sys_enter_write",即该函数将会在 "sys_enter_write" 系统调用的 Tracepoint 中执行。同时,该函数也被指定为一个 BPF Program 类型的 SEC,因为它是一个 BPF 程序的一部分。这些安全属性将会在 BPF 程序的加载和验证过程中得到验证,以确保程序的正确性和安全性。

bpf_get_current_pid_tgid() >> 32

bpf_get_current_pid_tgid() >> 32 的作用是将 bpf_get_current_pid_tgid() 函数返回的 64 位整数右移 32 位,得到当前进程的 PID。在 BPF 程序中,由于上下文环境的限制,只能使用 32 位的寄存器,因此需要使用右移操作将返回值转换为 32 位的整数,以便后续的处理。

用户程序部分

// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2020 Facebook */
#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);
}

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);

	/* 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;
}

这里有一个在源码中没有见过的头文件出现了

#include "minimal.skel.h"

这个部分暂时不去分析,等到将libbpf-bootstrap 框架用起来之后,再去探索框架本身的奥秘。

功能说明

这是一个用户态的bpf 程序,主要的作用就是把bpf程序加载进来,然后绑定到系统的tracepoint上,之后利用一个死循环不断地触发write 系统调用,用来让内核中的bpf程序进行工作。

后面还有一个cleanup 会在程序结束之后,清理bpf程序环境。

头文件引入介绍
  • stdio.h:包含标准输入输出库的函数和变量,例如 fprintf()、printf()、scanf() 等。
  • unistd.h:包含系统调用相关库的函数和变量,例如 getpid()、sleep() 等。
  • sys/resource.h:包含资源管理相关库的函数和变量,例如 getrusage() 等。
  • bpf/libbpf.h:包含 BPF 相关库的函数和变量,例如 libbpf_set_print()、libbpf_strerror() 等。
  • minimal.skel.h:预先生成的 BPF 程序头文件,包含 BPF 程序的结构体、函数和变量等。

其中minimal.skel.h 这个文件是由libbpf bootstrap 框架自动生成的,后面会详细讲解这个文件中会包含哪些api

libbpf_set_print

libbpf_set_print() 函数用于设置 BPF 相关库的输出信息和调试信息的回调函数。在这段代码中,调用 libbpf_set_print() 函数将自定义的回调函数 libbpf_print_fn() 传递给 BPF 相关库,以便在 BPF 相关库输出信息时将信息重定向到标准错误流中。

具体来说,libbpf_print_fn() 函数是一个静态函数,接受三个参数:输出日志的级别、输出日志的格式字符串和格式字符串的参数列表。在函数内部,调用 vfprintf() 函数将格式化的字符串和参数列表输出到标准错误流中。因此,调用 libbpf_set_print(libbpf_print_fn) 函数后,BPF 相关库的输出信息将被重定向到标准错误流中,以便在控制台上查看。

minimal_bpf__open

minimal_bpf__open() 函数是预先生成的 BPF 程序的接口之一,用于打开 BPF 程序的对象并返回一个指向该对象的指针。在这段代码中,调用 minimal_bpf__open() 函数打开预先生成的 BPF 程序对象,并将返回的指针赋值给 skel 变量。

在调用 minimal_bpf__open() 函数时,会自动进行一些初始化工作,例如动态加载 BPF 程序的 ELF 文件、创建 BPF 程序的对象等。如果打开成功,则返回一个指向 BPF 程序对象的指针;否则返回 NULL 指针表示打开失败。

skel->bss->my_pid = getpid();

首先通过 getpid() 函数获取当前进程的 PID,并将 PID 存储到 BPF 程序对象中定义的全局变量 my_pid 中。然后在 BPF 程序中,会以 my_pid 的值为条件判断系统调用事件的发起进程是否为当前进程,如果是则进行处理,否则忽略该事件。

minimal_bpf__load(skel);

minimal_bpf__load() 函数是预先生成的 BPF 程序的接口之一,用于加载和验证 BPF 程序。在加载和验证 BPF 程序时,minimal_bpf__load() 函数会执行以下操作:

  1. 调用 bpf_object__load() 函数加载 BPF 程序的 ELF 文件,并将 BPF 程序对象与 ELF 文件相关联。
  2. 调用 bpf_program__set_type() 函数设置 BPF 程序的类型。
  3. 调用 bpf_program__set_expected_attach_type() 函数设置 BPF 程序的预期挂载类型。
  4. 调用 bpf_object__attach_programs() 函数挂载 BPF 程序到指定的 Tracepoint 上。
  5. 调用 bpf_program__verify_attach() 函数验证 BPF 程序是否能够成功挂载到指定的 Tracepoint 上。

如果以上操作都执行成功,则返回 0;否则返回一个负数,表示加载和验证失败。

minimal_bpf__attach(skel);

minimal_bpf__attach() 函数是预先生成的 BPF 程序的接口之一,用于将 BPF 程序挂载到指定的 Tracepoint 上。在将 BPF 程序挂载到 Tracepoint 上时,minimal_bpf__attach() 函数会执行以下操作:

  1. 调用 bpf_object__find_program_by_name() 函数查找 BPF 程序对象中的指定程序。
  2. 调用 bpf_program__attach_tracepoint() 函数将 BPF 程序挂载到指定的 Tracepoint 上。

PS:其实这里就可以看出,所谓的框架生成的一些api,本质上就是对于原生的libbpf 程序接口的一些封装。

minimal_bpf__destroy(skel);

minimal_bpf__destroy() 函数是预先生成的 BPF 程序的接口之一,用于销毁预先生成的 BPF 程序对象。在销毁 BPF 程序对象时,minimal_bpf__destroy() 函数会执行以下操作:

调用 bpf_object__close() 函数关闭 BPF 程序对象,释放对象相关联的资源。

执行效果

PS: 如果希望可以打印出参数信息可以使用如下代码

SEC("tp/syscalls/sys_enter_write")
int handle_tp(struct trace_event_raw_sys_enter *args)
{
	int pid = bpf_get_current_pid_tgid() >> 32;

	if (pid != my_pid)
		return 0;

	bpf_printk("BPF triggered from PID %d.\n", pid);
    bpf_printk("write called with fd = %d and count = %d and content = %s\n", args->args[0], args->args[2],args->args[1]);

	return 0;
}

关于struct trace_event_raw_sys_enter 的信息,后面会详细介绍

07-16 02:30