使用 libbpf-bootstrap 编写eBPF程序

本文档详细介绍了如何使用libbpf-bootstrap编写eBPF程序,跟踪Linux系统中的进程启动和退出事件。Bootstrap是一个依赖BPF CO-RE的工具,通过加载BPF程序并使用BPF perf ring buffer传递事件。程序解析命令行参数,加载BPF程序,处理启动和退出事件,并收集进程信息。文章涵盖了内核态代码编写、用户态程序实现、Makefile配置以及与传统eBPF工具的比较。

以写一个Bootstrap.bpf.c为例

1.什么是Bootstrap?

bootstrap是我在现代BPF Linux环境中编写生产就绪BPF应用程序的方式。它依赖BPF CO-RE并且要求Linux内核使用CONFIG_DEBUG_INFO_BTF = y构建。

bootstrap跟踪exec()系统调用(使用SEC(“tp / sched / sched_process_exec”)handle_exit BPF程序),大致对应于新进程的生成(忽略简单起见的fork()部分)。此外,它还跟踪exit()(使用SEC(“tp / sched / sched_process_exit”)handle_exit BPF程序)以了解每个进程何时退出。这两个BPF程序共同工作,可以捕获关于任何新进程的有趣信息,如二进制文件名,以及在进程死亡时测量进程的寿命并收集有趣的统计信息,例如退出代码或消耗的资源量等。我发现这是深入了解内核内部并观察事情实际工作方式的好起点。

bootstrap还使用了libbpf库来帮助加载BPF程序,并且使用BPF CO-RE的新接口来加载和链接BPF程序。它还使用了BPF perf ring buffer,用于在内核和用户空间之间传递事件。

主函数首先定义了一个结构体env,用于存储程序选项,例如最小持续时间和详细模式。然后调用parse_arg函数解析命令行参数。

接下来,程序加载BPF程序并将其附加到跟踪进程启动和退出事件的内核位置。它还设置信号处理程序来捕获SIGINT和SIGTERM信号,并进入一个循环以轮询BPF perf ring buffer中的事件。如果收到事件,则将其传递给handle_event函数进行处理。循环继续进行,直到exiting标志被设置,此时程序进行清理并退出。

在handle_event函数中,程序检查事件的类型,然后收集有关进程信息的数据。如果进程启动事件,则创建一个新的进程记录,并将其存储在哈希表中。如果进程退出事件,则从哈希表中获取进程记录,并使用该信息打印有关进程的

2.写内核态代码

// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "bootstrap.h"

char LICENSE[] SEC("license") = "Dual BSD/GPL";

struct {
	__uint(type, BPF_MAP_TYPE_HASH);
	__uint(max_entries, 8192);
	__type(key, pid_t);
	__type(value, u64);
} exec_start SEC(".maps");

struct {
	__uint(type, BPF_MAP_TYPE_RINGBUF);
	__uint(max_entries, 256 * 1024);
} rb SEC(".maps");

const volatile unsigned long long min_duration_ns = 0;

SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
	struct task_struct *task;
	unsigned fname_off;
	struct event *e;
	pid_t pid;
	u64 ts;

	/* remember time exec() was executed for this PID */
	pid = bpf_get_current_pid_tgid() >> 32;
	ts = bpf_ktime_get_ns();
	bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);

	/* don't emit exec events when minimum duration is specified */
	if (min_duration_ns)
		return 0;

	/* reserve sample from BPF ringbuf */
	e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
	if (!e)
		return 0;

	/* fill out the sample with data */
	task = (struct task_struct *)bpf_get_current_task();

	e->exit_event = false;
	e->pid = pid;
	e->ppid = BPF_CORE_READ(task, real_parent, tgid);
	bpf_get_current_comm(&e->comm, sizeof(e->comm));

	fname_off = ctx->__data_loc_filename & 0xFFFF;
	bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);

	/* successfully submit it to user-space for post-processing */
	bpf_ringbuf_submit(e, 0);
	return 0;
}

SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
	struct task_struct *task;
	struct event *e;
	pid_t pid, tid;
	u64 id, ts, *start_ts, duration_ns = 0;
	
	/* get PID and TID of exiting thread/process */
	id = bpf_get_current_pid_tgid();
	pid = id >> 32;
	tid = (u32)id;

	/* ignore thread exits */
	if (pid != tid)
		return 0;

	/* if we recorded start of the process, calculate lifetime duration */
	start_ts = bpf_map_lookup_elem(&exec_start, &pid);
	if (start_ts)
		duration_ns = bpf_ktime_get_ns() - *start_ts;
	else if (min_duration_ns)
		return 0;
	bpf_map_delete_elem(&exec_start, &pid);

	/* if process didn't live long enough, return early */
	if (min_duration_ns && duration_ns < min_duration_ns)
		return 0;

	/* reserve sample from BPF ringbuf */
	e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
	if (!e)
		return 0;

	/* fill out the sample with data */
	task = (struct task_struct *)bpf_get_current_task();

	e->exit_event = true;
	e->duration_ns = duration_ns;
	e->pid = pid;
	e->ppid = BPF_CORE_READ(task, real_parent, tgid);
	e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
	bpf_get_current_comm(&e->comm, sizeof(e->comm));

	/* send data to user-space for post-processing */
	bpf_ringbuf_submit(e, 0);
	return 0;
}

        

这是一段使用BPF(Berkeley Packet Filter)的C程序,用于跟踪进程启动和退出事件,并显示有关它们的信息。BPF是一种强大的机制,允许您将称为BPF程序的小程序附加到Linux内核的各个部分。这些程序可用于过滤,监视或修改内核的行为。

程序首先定义一些常量,并包含一些头文件。然后定义了一个名为env的struct,用于存储一些程序选项,例如详细模式和进程报告的最小持续时间。

然后,程序定义了一个名为parse_arg的函数,用于解析传递给程序的命令行参数。它接受三个参数:一个表示正在解析的选项的整数key,一个表示选项参数的字符指针arg和一个表示当前解析状态的struct argp_state指针state。该函数处理选项并在env struct中设置相应的值。

然后,程序定义了一个名为sig_handler的函数,当被调用时会将全局标志exiting设置为true。这用于在接收到信号时允许程序干净地退出。

接下来,我们将继续描述这段代码中的其他部分。

程序定义了一个名为exec_start的BPF map,它的类型为BPF_MAP_TYPE_HASH,最大条目数为8192,键类型为pid_t,值类型为u64。

另外,程序还定义了一个名为rb的BPF map,它的类型为BPF_MAP_TYPE_RINGBUF,最大条目数为256 * 1024。

程序还定义了一个名为min_duration_ns的常量,其值为0。

程序定义了一个名为handle_exec的SEC(static evaluator of code)函数,它被附加到跟踪进程执行的BPF程序上。该函数记录为该PID执行exec()的时间,并在指定了最小持续时间时不发出exec事件。如果未指定最小持续时间,则会从BPF ringbuf保留样本并使用数据填充样本,然后将其提交给用户空间进行后处理。

程序还定义了一个名为handle_exit的SEC函数,它被附加到跟踪进程退出的BPF程序上。该函数会在确定PID和TID后计算进程的生命周期,然后根据min_duration_ns的值决定是否发出退出事件。如果进程的生命周期足够长,则会从BPF ringbuf保留样本并使用数据填充样本,然后将其提交给用户空间进行后处理。

最后,主函数调用bpf_ringbuf_poll来轮询BPF ringbuf,并在接收到新的事件时处理该事件。这个函数会持续运行,直到全局标志exiting被设置为true,此时它会清理资源并退出。

3.写用户态程序

// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2020 Facebook */
#include <argp.h>
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "bootstrap.h"
#include "bootstrap.skel.h"

static struct env {
	bool verbose;
	long min_duration_ms;
} env;

const char *argp_program_version = "bootstrap 0.0";
const char *argp_program_bug_address = "<bpf@vger.kernel.org>";
const char argp_program_doc[] =
"BPF bootstrap demo application.\n"
"\n"
"It traces process start and exits and shows associated \n"
"information (filename, process duration, PID and PPID, etc).\n"
"\n"
"USAGE: ./bootstrap [-d <min-duration-ms>] [-v]\n";

static const struct argp_option opts[] = {
	{ "verbose", 'v', NULL, 0, "Verbose debug output" },
	{ "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
	{},
};

static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
	switch (key) {
	case 'v':
		env.verbose = true;
		break;
	case 'd':
		errno = 0;
		env.min_duration_ms = strtol(arg, NULL, 10);
		if (errno || env.min_duration_ms <= 0) {
			fprintf(stderr, "Invalid duration: %s\n", arg);
			argp_usage(state);
		}
		break;
	case ARGP_KEY_ARG:
		argp_usage(state);
		break;
	default:
		return ARGP_ERR_UNKNOWN;
	}
	return 0;
}

static const struct argp argp = {
	.options = opts,
	.parser = parse_arg,
	.doc = argp_program_doc,
};

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
	if (level == LIBBPF_DEBUG && !env.verbose)
		return 0;
	return vfprintf(stderr, format, args);
}

static volatile bool exiting = false;

static void sig_handler(int sig)
{
	exiting = true;
}

static int handle_event(void *ctx, void *data, size_t data_sz)
{
	const struct event *e = data;
	struct tm *tm;
	char ts[32];
	time_t t;

	time(&t);
	tm = localtime(&t);
	strftime(ts, sizeof(ts), "%H:%M:%S", tm);

	if (e->exit_event) {
		printf("%-8s %-5s %-16s %-7d %-7d [%u]",
		       ts, "EXIT", e->comm, e->pid, e->ppid, e->exit_code);
		if (e->duration_ns)
			printf(" (%llums)", e->duration_ns / 1000000);
		printf("\n");
	} else {
		printf("%-8s %-5s %-16s %-7d %-7d %s\n",
		       ts, "EXEC", e->comm, e->pid, e->ppid, e->filename);
	}

	return 0;
}

int main(int argc, char **argv)
{
	struct ring_buffer *rb = NULL;
	struct bootstrap_bpf *skel;
	int err;

	/* Parse command line arguments */
	err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
	if (err)
		return err;

	libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
	/* Set up libbpf errors and debug info callback */
	libbpf_set_print(libbpf_print_fn);

	/* Cleaner handling of Ctrl-C */
	signal(SIGINT, sig_handler);
	signal(SIGTERM, sig_handler);

	/* Load and verify BPF application */
	skel = bootstrap_bpf__open();
	if (!skel) {
		fprintf(stderr, "Failed to open and load BPF skeleton\n");
		return 1;
	}

	/* Parameterize BPF code with minimum duration parameter */
	skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;

	/* Load & verify BPF programs */
	err = bootstrap_bpf__load(skel);
	if (err) {
		fprintf(stderr, "Failed to load and verify BPF skeleton\n");
		goto cleanup;
	}

	/* Attach tracepoints */
	err = bootstrap_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	/* Set up ring buffer polling */
	rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
	if (!rb) {
		err = -1;
		fprintf(stderr, "Failed to create ring buffer\n");
		goto cleanup;
	}

	/* Process events */
	printf("%-8s %-5s %-16s %-7s %-7s %s\n",
	       "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
	while (!exiting) {
		err = ring_buffer__poll(rb, 100 /* timeout, ms */);
		/* Ctrl-C will cause -EINTR */
		if (err == -EINTR) {
			err = 0;
			break;
		}
		if (err < 0) {
			printf("Error polling perf buffer: %d\n", err);
			break;
		}
	}

cleanup:
	/* Clean up */
	ring_buffer__free(rb);
	bootstrap_bpf__destroy(skel);

	return err < 0 ? -err : 0;
}

这是一个使用 Berkeley Packet Filter(BPF)在 Linux 系统上跟踪进程启动和退出事件的 C 程序的示例。它通过使用 libbpf 库来实现这一点,该库提供了用户空间与 BPF 程序和映射交互的接口。

该程序首先设置了 SIGINT 信号的信号处理程序,允许用户通过按 CTRL-C 终止程序。然后,它使用 argp 库解析命令行参数,允许用户指定是否启用详细调试输出以及要报告的进程的最小持续时间(以毫秒为单位)。

接下来,程序通过调用 libbpf_set_printfn() 函数来初始化 libbpf 库。该函数将在 libbpf 需要打印错误或调试消息时被调用。

然后,该程序通过调用 bootstrap_bpf 骨架中的 bootstrap_bpf__open() 函数加载并附加 BPF 程序,该骨架是使用 libbpfskeleton 工具从 BPF 程序生成的。BPF 程序通过将 kprobes 附加到 Linux 内核中的 do_execvedo_exit 函数来跟踪进程启动和退出事件。

最后,程序进入一个循环,等待在 BPF 程序的环形缓冲区映射中记录的事件。当记录事件时,它将从映射中读取并传递给 handle_event() 函数,该函数将事件信息打印到控制台。循环继续,直到退出标志被设置,这发生在接收到 SIGINT 信号时。当循环结束时,BPF 程序将被分离,并清理 libbpf 库。

4.写对应的Makefile

INCLUDES := -I$(OUTPUT)
CFLAGS := -g -Wall
ARCH := $(shell uname -m | sed 's/x86_64/x86/')

注释:变量 INCLUDES 被设置为 -I$(OUTPUT),这指定了编译器应该传递 -I 标志,后面跟着 OUTPUT 变量的值。-I 标志告诉编译器在指定目录中搜索头文件。

变量 CFLAGS 被设置为 -g -Wall,这指定了编译器应该传递 -g 和 -Wall 标志。-g 标志告诉编译器在对象文件中包含调试信息,这可用于使用调试器调试程序。-Wall 标志告诉编译器启用所有警告消息。

变量 ARCH 被设置为 uname -m 命令的输出,该命令返回计算机硬件架构的名称。然后使用 sed 命令将字符串 "x86_64" 替换为 "x86"。这使得 Makefile 可以支持 x86 和 x86_64 架构。

APPS = bootstrap

注释:变量 APPS 被设置为一个字符串列表:minimal 和 bootstrap。这个变量可能被用来指定使用 Makefile 可以构建的应用程序列表。

(1)构建libbpf


$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
	$(call msg,LIB,$@)
	$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1		      \
		    OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@)		      \
		    INCLUDEDIR= LIBDIR= UAPIDIR=			      \
		    install

注释:

$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
#

,指定如何构建目标 $(LIBBPF_OBJ)。目标的依赖项列在 : 右侧,构建目标的命令在接下来的行缩进。

目标 $(LIBBPF_OBJ) 的依赖项是存储在 OBJ_FILES 变量中的对象文件列表和 $(LIBBPF_SRC) 目录中的 Makefile。规则的 | $(OUTPUT)/libbpf 部分指定在构建目标之前应创建 $(OUTPUT)/libbpf 目录。

当处理此规则时,Make 会检查依赖项的时间戳,以确定是否需要构建目标。如果依赖项中有任何一个在上次构建目标后被修改过,Make 将执行构建目标的命令。如果依赖项没有被修改,Make 将跳过构建目标。


# $(call msg,LIB,$@)
#

在 Makefile 中,$(call ) 函数用于用参数展开变量或函数。

在这种情况下,$(call msg,LIB,$@) 展开为带有参数 LIB 和 $@ 的 msg 函数的值。msg 函数在 Makefile 中早些时候定义,并接受两个参数:消息类型和消息。它格式化消息并将其输出到控制台。

在 Makefile 中,$@ 变量是一个特殊变量,指向当前目标。在这种情况下,它将被替换为正在构建的目标的名称,即 $(LIBBPF_OBJ)

因此,调用 $(call msg,LIB,$@) 函数会向控制台输出类似于“正在构建 LIB $(LIBBPF_OBJ)”(假设 msg 函数以类似的方式格式化消息)的消息。


# $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1
#

这是一个 Makefile 规则中的命令,用于调用 make 命令来构建目标。

在 Makefile 中,$(Q) 变量是一个特殊变量,用于抑制命令的输出。如果 $(Q) 设置为空字符串,则命令将被输出到控制台。如果 $(Q) 设置为 @,则命令将不会输出到控制台。

在 Makefile 中,$(MAKE) 变量是一个特殊变量,指向 make 命令。这允许您在需要时使用不同版本的 make

-C $(LIBBPF_SRC) 标志告诉 make 在构建目标之前更改到 $(LIBBPF_SRC) 目录。BUILD_STATIC_ONLY=1 标志告诉 make 只构建库的静态版本。

因此,此命令将在 $(LIBBPF_SRC) 目录中调用 make 命令,并设置 BUILD_STATIC_ONLY=1 标志,如果 $(Q) 设置为 @,则命令的输出将被抑制。

#

OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@)

"

在这个 Makefile 规则中,OBJDIR 和 DESTDIR 变量作为参数传递给 make 命令。

$(dir $@) 语法是 Makefile 函数,返回当前目标名称的目录部分。在这种情况下,$@ 指的是 $(LIBBPF_OBJ) 目标,所以 $(dir $@) 将返回 $(LIBBPF_OBJ) 文件所在的目录。

OBJDIR 和 DESTDIR 变量由 make 命令使用,指定将中间对象文件和最终目标文件分别放置在哪里。OBJDIR 变量指定中间对象文件的目录,DESTDIR 变量指定最终目在这种情况下,OBJDIR 变量被设置为 $(dir $@)/libbpf,这指定中间对象文件应该放在 $(LIBBPF_OBJ) 目标所在目录的 libbpf 子目录中。DESTDIR 变量被设置为 $(dir $@),这指定最终目标文件应该放在与 $(LIBBPF_OBJ) 目标相同的目录中。

"

INCLUDEDIR= LIBDIR= UAPIDIR=

install

在这个 Makefile 规则中,INCLUDEDIRLIBDIR 和 UAPIDIR 变量作为参数传递给 make 命令。它们用于指定应该安装包含文件、库文件和用户空间 API 文件的目录。

在这种情况下,所有这些变量都设置为空字符串,这意味着 make 命令将使用这些文件的默认目录。默认目录通常在 libbpf 源代码目录中的 Makefile 中指定,通常是系统特定的目录,例如 /usr/include/usr/lib 和 /usr/share/libbpf

install 目标是 libbpf Makefile 中的预定义目标,负责安装包含文件、库文件和用户空间 API 文件。当使用 make 命令调用 install 目标时,它会将必要的文件复制到指定的目录中。

在这种情况下,使用 make 命令调用 install 目标,并将 INCLUDEDIRLIBDIR 和 UAPIDIR 变量作为参数传递,以指定应该安装包含文件、库文件和用户空间 API 文件的目录。由于这些变量被设置为

空字符串,因此 install 目标将使用这些文件的默认目录。当使用 make 命令调用 install 目标时,它将将必要的文件复制到 libbpf Makefile 中指定的默认目录中。

(2)构建BPF Code

$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) vmlinux.h | $(OUTPUT)
	$(call msg,BPF,$@)
	$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) -c $(filter %.c,$^) -o $@
	$(Q)$(LLVM_STRIP) -g $@ # strip useless DWARF info
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) vmlinux.h | $(OUTPUT)
"

这是一个用于从 BPF(Berkeley Packet Filter)源文件构建 BPF(Berkeley Packet Filter)对象文件的规则。输出文件被指定为 $(OUTPUT)/%.bpf.o,其中 $(OUTPUT) 是指定输出目录的变量,而 %.bpf.o 是匹配 BPF 对象文件名称的模式。

此规则的输入文件被指定为 %.bpf.c,即 BPF 源文件,$(LIBBPF_OBJ),即包含 libbpf 库的对象代码的文件,$(wildcard%.h)

即匹配任何头文件的模式,以及 vmlinux.h,即头文件。规则的 | $(OUTPUT) 部分指定如果 $(OUTPUT) 目录不存在,则应创建该目录。

此规则指定输出文件应使用其后的命令从输入文件构建。该命令通常是编译器或构建工具,可使用输入文件生成输出文件。

在本例中,命令用于使用 Clang 编译器从 BPF 源文件构建 BPF 对象文件。

%.bpf.c 是匹配 BPF 源文件的模式。  字符是通配符,匹配任何字符,因此此模式将匹配任何具有 .bpf.c 扩展名的文件。例如,example.bpf.c 和 foo.bpf.c 都将匹配此模式。

$(LIBBPF_OBJ) 是指定包含 libbpf 库的对象代码的文件名的变量。此文件中的对象代码是从源代码编译的,可与其他对象文件链接以创建可执行程序。

在此上下文中,%.bpf.c 和 $(LIBBPF_OBJ) 用作构建规则的输入文件。规则指定输出文件应使用其后的命令从这些输入文件构建。该命令通常是编译器或构建工具,可使用输入文件生成输出文件。
" $(call msg,BPF,$@)

$(call msg,BPF,$@) 是对 msg 函数的函数调用,带有两个参数:BPF 和 $@

msg 函数是在 Makefile 的其他地方定义的自定义函数。它用于将消息打印到控制台。第一个参数 BPF 是要打印的消息,第二个参数 $@ 是正在构建的目标文件的名称。

在此上下文中,调用 msg 函数打印指示正在构建 BPF 对象文件的消息。打印的消息将是类似于“构建 BPF 对象文件:output/example.bpf.o”的消息。 $@ 变量将被替换为正在构建的 BPF 对象文件的名称。

$(call) 函数是 Make 中的内置函数,允许您使用可变数量的参数调用用户定义的函数。 $(call) 的第一个参数是要调用的函数的名称,剩余的参数作为参数传递给函数。


“ $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) -c $(filter %.c,$^) -o $@
"

这是一条用于从 BPF(Berkeley Packet Filter)源文件构建 BPF(Berkeley Packet Filter)对象文件的命令。

该命令使用 Clang 编译器,该编译器由 $(CLANG) 变量指定。

-g 标志使调试信息包含在对象文件中。

-O2 标志启用优化。

-target bpf 标志指定目标架构为 BPF。

-D__TARGET_ARCH_$(ARCH) 标志定义了指定目标架构的宏。

$(INCLUDES) 变量指定附加的包含目录。

-c 标志指示编译器应生成对象文件,

-o 标志指定输出文件。

编译器的输入文件被指定为 $(filter%.c,$^),这是一个过滤器,仅从输入文件列表(指定为 $^)中选择源文件(即具有 .c 扩展名的文件)。

输出文件被指定为 $@,这是 Make 中的内置变量,表示目标文件的名称。

$(Q) 变量用于控制命令的打印。如果 $(Q) 为空,则该命令将打印到控制台。如果 $(Q) 设置为 @,则不会打印该命令。


" $(Q)$(LLVM_STRIP) -g $@ # strip useless DWARF info

"

这是一条用于从 BPF(Berkeley Packet Filter)对象文件中剥离调试信息的命令。该命令使用 llvm-strip 工具,该工具由 $(LLVM_STRIP) 变量指定。

-g 标志指定应剥离调试信息。

输入文件被指定为 $@,这是 Make 中的内置变量,表示目标文件的名称。

$(Q) 变量用于控制命令的打印。如果 $(Q) 为空,则该命令将打印到控制台。如果 $(Q) 设置为 @,则不会打印该命令。

# strip useless DWARF info 部分是解释命令目的的注释。DWARF(带属性记录格式调试)是对象文件中调试信息的格式。可以使用 llvm-strip 工具从对象文件中删除调试信息,以减小文件大小或删除敏感信息。在这种情况下,llvm-strip 工具正在用于从 BPF 对象文件中删除被认为是“无用的”的调试信息。

(3)生成BPF skeletons

$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT)
	$(call msg,GEN-SKEL,$@)
	$(Q)$(BPFTOOL) gen skeleton $< > $@
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT)
	$(call msg,GEN-SKEL,$@)
"

$(call msg,GEN-SKEL,$@) 是 Makefile 中的一个函数调用,用于打印指示正在生成 BPF 骨架头文件的消息。GEN-SKEL 参数指定要打印的消息,$@ 参数指定目标文件的名称。

$(call) 函数是 Make 中的内置函数,允许您使用可变数量的参数调用函数。$(call) 的第一个参数是函数的名称,其余参数作为附加参数传递给函数。

在这种情况下,使用两个参数调用了 msg 函数:GEN-SKEL 和 $@msg 函数是在 Makefile 中的其他位置定义的自定义函数。它用于向控制台打印消息。GEN-SKEL 参数指定要打印的消息,$@ 参数指定目标文件的名称。

例如,如果目标文件是 output/example.skel.h,函数调用将向控制台打印消息“生成 example.skel.h”。

" $(Q)$(BPFTOOL) gen skeleton $< > $@
"

$(Q)$(BPFTOOL) gen skeleton $< > $@ 是 Makefile 中的一条命令,用于从 BPF(Berkeley Packet Filter)对象文件生成 BPF 骨架头文件。

$(BPFTOOL) 变量指定了 bpftool 实用程序,该实用程序是用于操纵 BPF 程序和映射的工具。gen skeleton 子命令从 BPF 对象文件生成 BPF 骨架头文件。

输入文件被指定为 $<,这是 Make 中的内置变量,表示规则的第一个前置文件(即输入文件)。在这种情况下,输入文件是从 BPF 源文件生成的 BPF 对象文件。

输出文件被指定为 > $@,这将命令的输出重定向到目标文件。$@ 变量是 Make 中的内置变量,表示目标文件的名称。

$(Q) 变量控制命令的打印。如果 $(Q) 为空,则该命令将打印到控制台。如果 $(Q) 设置为 @,则不会打印该命令。

BPF 骨架头文件是头文件,包含 BPF 函数和数据结构的定义和声明。它们可以使用 bpftool 实用程序从 BPF 对象文件生成。它们可以在 C 或 C++ 源文件中包含,以从对象文件访问 BPF 函数和数据结构。

(4)构建用户态代码

$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h

$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
	$(call msg,CC,$@)
	$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@

$(call msg,GEN-SKEL,$@) 是 Makefile 中的一个函数调用,用于打印指示正在生成 BPF 骨架头文件的消息。GEN-SKEL 参数指定要打印的消息,$@ 参数指定目标文件的名称。

$(call) 函数是 Make 中的内置函数,允许您使用可变数量的参数调用函数。$(call) 的第一个参数是函数的名称,其余参数作为附加参数传递给函数。

在这种情况下,使用两个参数调用了 msg 函数:GEN-SKEL 和 $@msg 函数是在 Makefile 中的其他位置定义的自定义函数。它用于向控制台打印消息。GEN-SKEL 参数指定要打印的消息,$@ 参数指定目标文件的名称。

例如,如果目标文件是 output/example.skel.h,函数调用将向控制台打印消息“生成 example.skel.h”。

" $(Q)$(BPFTOOL) gen skeleton $< > $@
"

$(Q)$(BPFTOOL) gen skeleton $< > $@ 是 Makefile 中的一条命令,用于从 BPF(Berkeley Packet Filter)对象文件生成 BPF 骨架头文件。

$(BPFTOOL) 变量指定了 bpftool 实用程序,该实用程序是用于操纵 BPF 程序和映射的工具。gen skeleton 子命令从 BPF 对象文件生成 BPF 骨架头文件。

输入文件被指定为 $<,这是 Make 中的内置变量,表示规则的第一个前置文件(即输入文件)。在这种情况下,输入文件是从 BPF 源文件生成的 BPF 对象文件。

输出文件被指定为 > $@,这将命令的输出重定向到目标文件。$@ 变量是 Make 中的内置变量,表示目标文件的名称。

$(Q) 变量控制命令的打印。如果 $(Q) 为空,则该命令将打印到控制台。如果 $(Q) 设置为 @,则不会打印该命令。

BPF 骨架头文件是头文件,包含 BPF 函数和数据结构的定义和声明。它们可以使用 bpftool 实用程序从 BPF 对象文件生成。它们可以在 C 或 C++ 源文件中包含,以从对象文件访问 BPF 函数和数据结构。

"

# Build user-space code
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
"
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
"

规则 $(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h 指定了目标文件和其前置文件。

目标文件是从 $(APPS) 变量中的源文件生成的对象文件列表。使用 $(patsubst) 函数生成对象文件列表。它通过用替换字符串 $(OUTPUT)/%.o 替换 $(APPS) 中的单词列表中的模式 % 来执行字符串替换。

例如,如果 $(APPS) 定义为 example1 example2,则生成的对象文件列表将是 $(OUTPUT)/example1.o $(OUTPUT)/example2.o

前置文件是从 BPF 对象文件生成的骨架头文件。目标 %.o 是一个匹配任何对象文件的模式规则,前置文件 %.skel.h 是一个匹配任何骨架头文件的模式规则。

此规则告诉 Make,列表中的对象文件是从相应的骨架头文件生成的。当目标对象文件过时或不存在时,Make 将检查相应的骨架头文件,以确定是否需要从 BPF 对象文件重新生成。如果骨架头文件过时或不存在,Make 将在构建目标对象文件之前重新生成它。


" $(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
"

规则 $(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) 指定了目标文件和其前置文件。

目标文件是从 C 源文件生成的对象文件。目标 %.o 是一个匹配任何对象文件的模式规则。

前置文件是 C 源文件和它包含的任何头文件。前置文件 %.c 是一个匹配任何 C 源文件的模式规则,函数 $(wildcard %.h) 扩展为当前目录中的所有头文件列表。

规则的 | $(OUTPUT) 部分指定了仅顺序前置文件。它告诉 Make,必须在构建目标对象文件之前创建 $(OUTPUT) 目录,但目录的修改时间不会影响重新构建目标的决策。

此规则告诉 Make,对象文件是从 C 源文件和它包含的任何头文件生成的。当目标对象文件过时或不存在时,Make 将检查 C 源文件和头文件,以确定是否需要重新编译。如果任何前置文件过时或不存在,Make 将重新构建目标对象文件。

" $(call msg,CC,$@)
"

函数调用 $(call msg,CC,$@) 用于打印指示正在编译 C 对象文件的消息。

函数 $(call) 是 Make 的内置函数,允许您使用可变数量的参数调用函数。函数 $(call) 的第一个参数是要调用的函数的名称,其余参数作为参数传递给函数。

在这种情况下,函数 msg 带有参数 CC 和 $@ 被调用。参数 CC 指定要打印的消息,参数 $@ 指定目标文件的名称。

函数 msg 不是 Make 的内置函数,因此可能在 Makefile 中更早地定义。它用于向用户打印带有指示消息类型的前缀的消息。参数 CC 指定消息是与 C 编译相关的消息。

此函数调用用于通知用户正在编译 C 对象文件。通这个函数通常用于调试或向用户提供有关构建过程的信息。

" $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
"

命令 $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ 用于将 C 源文件编译成对象文件。

变量 $(Q) 用于控制是否将命令行打印给用户。如果 $(Q) 设置为空字符串,则将命令行打印给用户。如果 $(Q) 设置为 @,则不会打印命令行。

变量 $(CC) 指定要使用的 C 编译器。变量 $(CFLAGS) 指定要传递给 C 编译器的任何其他标志。变量 $(INCLUDES) 指定 C 编译器要使用的任何其他包含目录。

标志 -c 告诉 C 编译器将源文件编译成对象文件,但不进行链接。标志 -o 指定输出文件的名称。

函数 $(filter %.c,$^) 扩展为前置文件列表中的所有 C 源文件列表。变量 $^ 扩展为所有前置文件的列表。函数 $(filter) 用于从列表中选择 C 源文件。

此命令用于将 C 源文件编译成对象文件。对象文件将用作链接最终可执行文件的前置文件。

(5)构建二进制可执行程序

$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
	$(call msg,BINARY,$@)
	$(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o $@

规则 $(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) 指定了要构建的目标文件和所需的前置文件。

目标文件是所有要构建的应用程序二进制文件。变量 $(APPS) 包含所有要构建的应用程序二进制文件的列表。目标模式 % 是一个通配符,匹配任何要构建的应用程序二进制文件。

前置文件包括对象文件和库对象文件。前置文件 $(OUTPUT)/%.o 是从与应用程序二进制文件具有相同基本名称的 C 源文件构建的对象文件。前置文件 $(LIBBPF_OBJ) 是所有库对象文件的列表。

规则的 | $(OUTPUT) 部分指定了仅按顺序的前置文件。这意味着在构建目标应用程序二进制文件之前,Make 必须创建 $(OUTPUT) 目录,但目录的修改时间不会影响是否重新构建目标。

这个规则告诉 Make,所有要构建的应用程序二进制文件都依赖于对象文件和库对象文件。当应用程序二进制文件过时或不存在时,Make 会检查对象文件和库对象文件,确定是否需要重新构建它们。如

果需要重新构建任何前置文件,Make 将重新构建目标应用程序二进制文件。

如果所有前置文件都存在且是最新的,Make 将执行用于构建应用程序二进制文件的命令。这些命令通常包括链接器命令,用于将对象文件和库对象文件链接在一起,以创建可执行的应用程序二进制文件。


" $(call msg,BINARY,$@)
"

$(call msg,BINARY,$@) 是一个函数调用,用于显示构建目标应用程序二进制文件的消息。

这个函数的第一个参数 "BINARY" 是消息的类型。第二个参数 $@ 是目标文件的名称。

这个函数的具体实现取决于 Makefile 中如何定义。例如,它可能会在 Makefile 中使用以下内容定义:


Copy code

define msg @echo "($1) $2" endef

在这种情况下,每次调用 $(call msg,TYPE,FILE) 时,函数都会在控制台中输出 "($TYPE) $FILE"。

例如,如果调用 $(call msg,BINARY,app.out),函数会在控制台中输出 "(BINARY) app.out"。

" $(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o $@
"

5.运行程序

make  bootstrap

sudo ./bootstrap -d 50

6对比eunomia-bpf

1.,只需要编写内核态代码即可自动获取内核态导出的数据,编译后即可进行加载和运行,降低了 eBPF 的学习成本,提高了开发效率。

 2.基于 libbpf一次编译处处运行的特性,将用户态、内核态的编译和运行的完全分离,通过标准 JSON 或 WASM模块的方式进行分发,无需进行重新编译,应用启动占用资源少,时间短,甚至容器启动更短。

3.只编写内核态代码的时候,使用 JSON 即可完成分发、加载、打包的过程,对于完整的、需要用户态和内核态进行交互的 eBPF 应用或工具,可以在 WASM 中编写复杂的用户态处理程序进行控制和处理,并且将编译好的 eBPF 字节码嵌入在 WASM 模块中一同分发,在目标机器上动态加载运行。
 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值