深入探析 eBPF:从程序编写到执行的全流程解析

文摘   2024-09-28 17:42   新加坡  

作者简介:杨月顺,西安邮电大学研二在读,陈莉君教授学生。操作系统和Linux内核爱好者,热衷于探索Linux内核和eBPF技术。

在现代Linux内核中,eBPF(Extended Berkeley Packet Filter)已经成为强大的工具,用于高效地在内核中执行定制代码,帮助用户监控、分析和优化系统性能。然而,eBPF的执行机制背后蕴含着一套复杂的流程:从编写用户态程序、编译、加载到内核,验证,再到最终的执行,涉及多个关键步骤。在这篇文章中,我将从eBPF用户态代码开始分析,一步一步地去分析eBPF程序从编写代码到最终执行的整个过程。并且通过分析libbpf、bpftrace以及内核的源代码和一些调试信息,让大家对eBPF程序的编译、加载、验证、执行有一个清晰且完整的认识。

本文将重点从以下四个方面来分析eBPF的执行原理:

  1. 编译:分析eBPF程序的编译过程。

  2. 加载:分析eBPF程序的加载过程。

  3. 验证:分析BPF虚拟机如何进行验证。

  4. JIT:分析eBPF字节码如何转化为本地机器指令。

首先,先编写以下两个ebpf程序:

1.使用libbpf库来编写eBPF程序:(hello_kern.c)

#include <linux/bpf.h>
#define SEC(NAME) __attribute__((section(NAME), used))

static int (*bpf_trace_printk)(const char *fmt, int fmt_size,
                              ...) = (void *)BPF_FUNC_trace_printk;

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

以上程序实现了一个简单的打印功能,我们后面会详细对这个程序进行分析。

2.使用bpftrace编写eBPF程序:

bpftrace -e 'kprobe:do_nanosleep { printf("PID %d sleeping...\n", pid); }'

这个bpftrace程序追踪了do_nanosleep 内核函数(该函数会在进程睡眠时触发),并打印出触发该函数的进程PID。

写好了这两个程序,接下来我会通过这两个例子来详细介绍eBPF程序的整个执行过程。首先,我会使用一张图来给大家宏观介绍一下eBPF程序的执行流程。

这张图是eBPF官方给出的图,接下来我会从图中标注的四个重点步骤来简要说明一下:

1.编译:

  • eBPF 程序以 C 语言编写,使用 clang -target bpf 编译器将 C 源代码编译为 eBPF 字节码。这个字节码是内核可以理解并执行的代码格式。

  • 编译之后,生成的 eBPF 程序字节码和与之相关的 eBPF 映射(Maps)可以通过用户态程序进行管理。

2.加载:

  • 编译生成的 eBPF 程序会通过 libbpf 库加载到 Linux 内核中。这个步骤涉及使用 bpf() 系统调用将 eBPF 程序提交给内核进行处理。

  • 同时,eBPF Maps(存储 eBPF 程序运行时数据的结构)也会在用户态与内核之间进行交互。

3.验证:

  • 在程序加载后,eBPF 验证器会检查字节码,确保程序的安全性和合法性,防止 eBPF 程序破坏内核的稳定性。验证通过后,eBPF 程序通过 eBPF JIT(Just-In-Time)编译器进一步优化,将字节码转换为机器码以提升执行效率。

4.JIT:

  • 通过 JIT,eBPF 字节码被直接编译成底层的机器代码,程序可以直接在 CPU 上执行,无需经过解释过程。

有了上述的介绍,想必大家对eBPF程序的执行过程已经有了一个大致的认识了吧,接下来我们开始详细分析每个关键步骤。

一、编译阶段:

由于使用libbpf库编写的ebpf程序和使用bpftrace编写的eBPF程序在编译阶段的过程有一些不同,因此在本章节中我会说明每个工具在编译过程中特有的内容。

在编译阶段所做的事情可以总结为一句话:将用户写好的eBPF程序编译成eBPF字节码。那么什么是eBPF字节码?话不多是,直接看图:

//1.使用clang编译器对我们编写的hello_kern.c源代码进行编译:
clang -O2 -target bpf -c hello_kern.c -o hello_kern.o
//2.输出 eBPF 字节码和汇编指令
llvm-objdump -S hello_kern.o

如上图所示,每行中的左边一串16进制内容就是eBPF的指令字节码,右边是汇编指令。

接下来我们来分析Clang/LLVM是如何将ebpf的.c程序编译成字节码文件的。

1.1 Clang/LLVM编译eBPF程序过程

操作工具功能
clangClang负责将C源码(如hello_kern.c)编译成LLVM中间表示(LLVM IR),-target bpf标志表明目标架构是BPF(用于生成eBPF字节码)。命令中的-Wall启用所有警告,-O2用于优化代码以提高性能。
opt(高层优化)这一步通过多次优化(Pass 1到Pass N)对生成的LLVM IR进行高层次优化。这些优化可以包括代码清理、消除冗余代码、循环优化等,从而提高生成代码的效率和性能。
llc(LLVM静态编译器)LLC将优化后的LLVM IR转换为最终的二进制格式(即BPF字节码)。该字节码最终以ELF格式输出,适用于eBPF程序在内核空间中的加载和执行。
binaryeBPF字节码。

上述内容分析了通过Clang编译器来进行编译的详细过程,接下来我们查看一下编译好的字节码文件中的内容(ELF文件内容):

//llvm-readelf用于读取和显示ELF文件信息的工具,-S表示显示ELF文件中的节头,-s用于显示ELF文件中的符号表
llvm-readelf -S -s hello_kern.o

通过上述内容我们可以看到eBPF字节码文件的节头和符号表信息,里面包含了我们编写的程序通过编译后的各项信息。比如:在这里我们可以看到SEC("tracepoint/syscalls/sys_enter_execve")代码对应的Section信息,通过查看符号表,可以看到bpf_prog函数符号,大小为112个字节,位于第三个节 (Ndx = 3,对应 tracepoint/syscalls/sys_enter_execve)。

上述所讲解的Clang/LLVM编译eBPF程序的过程是libbpf库和bpftrace使用Clang/LLVM编译eBPF程序的共有过程,但是编译使用bpftrace编写的eBPF程序还需要在这个过程之前有一些操作,接下来详细解释。

1.2 bpftrace编译过程

libbpf编写的程序会通过clang 编译器将 C 代码转化为 eBPF 字节码。而bpftrace 程序的编译经过AST,LLVM IR(1.1分析内容),BPF bytecode这几个阶段,而接下来我会补充AST这个阶段:

为了方便讲解,在这里使用图片来展示bpftrace的编译加载等过程:

简单来讲,在AST这个阶段就是将我们使用bpftrace编写的程序转化为AST格式。然后进行语义分析等工作,最终交给Clang/LLVM工作,最终形成eBPF字节码。那么通过AST处理后的最终结果是什么呢?请看下图:

// AST 的结构示意可以使用 -d 查看
bpftrace -d -e 'kprobe:do_nanosleep { printf("PID %d sleeping...\n", pid); }'

那么这个结果是怎么获得的呢?

上面这张图展示了从编写的bpftrace程序到AST结构的处理流程,接下来我会通过如何解析pid这个字段来从源码的角度说明这个过程:

1.首先,由于使用了 pid(bpftrace 内置变量),因此在 lex 查表过程中识别为 builtin:(bpftrace/src/lexer.l)

ident    [_a-zA-Z][_a-zA-Z0-9]*
map      @{ident}|@
var      ${ident}
hspace   [ \t]
vspace   [\n\r]
space   {hspace}|{vspace}
path     :(\\.|[_\-\./a-zA-Z0-9#+\*])+
builtin  arg[0-9]|args|cgroup|comm|cpid|numaid|cpu|ctx|curtask|elapsed
|func|gid|pid|probe|rand|retval|sarg[0-9]|tid|uid|username|jiffies

2.进一步在 yacc 中匹配BULITIN就能分配一个关于 pid 的 AST 结点。

primary_expr:
              IDENT             { $$ = driver.ctx.make_node<ast::Identifier>($1, @$); }
      |       int               { $$ = $1; }
      |       STRING             { $$ = driver.ctx.make_node<ast::String>($1, @$); }
      |       STACK_MODE         { $$ = driver.ctx.make_node<ast::StackMode>($1, @$); }
      |       BUILTIN           { $$ = driver.ctx.make_node<ast::Builtin>($1, @$); }
      |       CALL_BUILTIN       { $$ = driver.ctx.make_node<ast::Builtin>($1, @$); }
      |       LPAREN expr RPAREN { $$ = $2; }
      |       param             { $$ = $1; }
      |       map_or_var         { $$ = $1; }
      |       "(" vargs "," expr ")"
              {
                auto &args = $2;
                args.push_back($4);
                $$ = driver.ctx.make_node<ast::Tuple>(std::move(args), @$);
              }
              ;

最终,我们就获取到了AST结构信息。再通过语义分析和LLVM/Clang的处理就可以得到eBPF字节码文件了。

二、加载阶段:

通过编译阶段得到了eBPF字节码文件,接下来我们来分析eBPF字节码是如何加载进内核的。本章节我会通过libbpf的源码来分析这个阶段:(libbpf/src/bpf.c)

int bpf_prog_load(enum bpf_prog_type prog_type,
 const char *prog_name, const char *license,
 const struct bpf_insn *insns, size_t insn_cnt,
 struct bpf_prog_load_opts *opts)
{
const size_t attr_sz = offsetofend(union bpf_attr, prog_token_fd);
void *finfo = NULL, *linfo = NULL;
const char *func_info, *line_info;
__u32 log_size, log_level, attach_prog_fd, attach_btf_obj_fd;
__u32 func_info_rec_size, line_info_rec_size;
int fd, attempts;
union bpf_attr attr;
char *log_buf;
// 增加系统的 memlock 资源限制
bump_rlimit_memlock();
// 检查传入的选项是否有效
if (!OPTS_VALID(opts, bpf_prog_load_opts))
return libbpf_err(-EINVAL);
// 尝试次数选项处理,若尝试次数小于 0 返回错误,若为 0 则设置为默认尝试次数
attempts = OPTS_GET(opts, attempts, 0);
if (attempts < 0)
return libbpf_err(-EINVAL);
if (attempts == 0)
attempts = PROG_LOAD_ATTEMPTS;
// 初始化 bpf_attr 结构体
memset(&attr, 0, attr_sz);
// 设置 eBPF 程序的类型和附加选项
attr.prog_type = prog_type;
attr.expected_attach_type = OPTS_GET(opts, expected_attach_type, 0);
// 设置 BTF 和其他相关的选项
attr.prog_btf_fd = OPTS_GET(opts, prog_btf_fd, 0);
attr.prog_flags = OPTS_GET(opts, prog_flags, 0);
attr.prog_ifindex = OPTS_GET(opts, prog_ifindex, 0);
attr.kern_version = OPTS_GET(opts, kern_version, 0);
attr.prog_token_fd = OPTS_GET(opts, token_fd, 0);
// 如果程序名称存在并且支持功能特性,则复制程序名称
if (prog_name && feat_supported(NULL, FEAT_PROG_NAME))
libbpf_strlcpy(attr.prog_name, prog_name, sizeof(attr.prog_name));
attr.license = ptr_to_u64(license);
// 指令数量检查,如果指令数量超过 UINT_MAX 则返回错误
if (insn_cnt > UINT_MAX)
return libbpf_err(-E2BIG);
// 设置指令和指令数量
attr.insns = ptr_to_u64(insns);
attr.insn_cnt = (__u32)insn_cnt;
// 获取附加的程序 FD 和 BTF 对象 FD
attach_prog_fd = OPTS_GET(opts, attach_prog_fd, 0);
attach_btf_obj_fd = OPTS_GET(opts, attach_btf_obj_fd, 0);
// 检查 attach_prog_fd 和 attach_btf_obj_fd 的冲突情况
if (attach_prog_fd && attach_btf_obj_fd)
return libbpf_err(-EINVAL);
// 设置 BTF 附加 ID 和相应的 FD
attr.attach_btf_id = OPTS_GET(opts, attach_btf_id, 0);
if (attach_prog_fd)
attr.attach_prog_fd = attach_prog_fd;
else
attr.attach_btf_obj_fd = attach_btf_obj_fd;
// 获取日志缓冲区和日志选项
log_buf = OPTS_GET(opts, log_buf, NULL);
log_size = OPTS_GET(opts, log_size, 0);
log_level = OPTS_GET(opts, log_level, 0);
// 确保日志缓冲区和日志大小的一致性
if (!!log_buf != !!log_size)
return libbpf_err(-EINVAL);
// 获取函数信息和行信息的记录大小
func_info_rec_size = OPTS_GET(opts, func_info_rec_size, 0);
func_info = OPTS_GET(opts, func_info, NULL);
attr.func_info_rec_size = func_info_rec_size;
attr.func_info = ptr_to_u64(func_info);
attr.func_info_cnt = OPTS_GET(opts, func_info_cnt, 0);
line_info_rec_size = OPTS_GET(opts, line_info_rec_size, 0);
line_info = OPTS_GET(opts, line_info, NULL);
attr.line_info_rec_size = line_info_rec_size;
attr.line_info = ptr_to_u64(line_info);
attr.line_info_cnt = OPTS_GET(opts, line_info_cnt, 0);
// 设置 fd 数组
attr.fd_array = ptr_to_u64(OPTS_GET(opts, fd_array, NULL));
// 设置日志相关的选项,如果启用了日志
if (log_level) {
attr.log_buf = ptr_to_u64(log_buf);
attr.log_size = log_size;
attr.log_level = log_level;
}
// 调用系统调用加载 eBPF 程序
fd = sys_bpf_prog_load(&attr, attr_sz, attempts);
OPTS_SET(opts, log_true_size, attr.log_true_size);
if (fd >= 0)
return fd;
// 如果加载失败且 errno 为 E2BIG,尝试调整 func_info 和 line_info 并重试加载
while (errno == E2BIG && (!finfo || !linfo)) {
if (!finfo && attr.func_info_cnt &&
   attr.func_info_rec_size < func_info_rec_size) {
// 修正 func_info 并重新尝试
finfo = alloc_zero_tailing_info(func_info,
attr.func_info_cnt,
func_info_rec_size,
attr.func_info_rec_size);
if (!finfo) {
errno = E2BIG;
goto done;
}

attr.func_info = ptr_to_u64(finfo);
attr.func_info_rec_size = func_info_rec_size;
} else if (!linfo && attr.line_info_cnt &&
  attr.line_info_rec_size < line_info_rec_size) {
// 修正 line_info 并重新尝试
linfo = alloc_zero_tailing_info(line_info,
attr.line_info_cnt,
line_info_rec_size,
attr.line_info_rec_size);
if (!linfo) {
errno = E2BIG;
goto done;
}
attr.line_info = ptr_to_u64(linfo);
attr.line_info_rec_size = line_info_rec_size;
} else {
break;
}
// 再次尝试加载 eBPF 程序
fd = sys_bpf_prog_load(&attr, attr_sz, attempts);
OPTS_SET(opts, log_true_size, attr.log_true_size);
if (fd >= 0)
goto done;
}
// 如果没有启用日志,但提供了日志缓冲区,重新尝试加载并启用日志
if (log_level == 0 && log_buf) {
attr.log_buf = ptr_to_u64(log_buf);
attr.log_size = log_size;
attr.log_level = 1;
fd = sys_bpf_prog_load(&attr, attr_sz, attempts);
OPTS_SET(opts, log_true_size, attr.log_true_size);
}
done:
// 释放分配的内存
free(finfo);
free(linfo);
return libbpf_err_errno(fd);
}

这段代码 bpf_prog_loadlibbpf 库中用于加载 eBPF 程序到内核的重要函数,它执行了一系列步骤,将编译好的 eBPF 程序通过系统调用加载到内核中,并附加到特定的内核事件或子系统上。以下是程序中的一些关键部分及其解释:

1.bump_rlimit_memlock()函数:

  • 这个函数提高了 RLIMIT_MEMLOCK 限制,确保在加载 eBPF 程序时有足够的锁定内存可用。加载 eBPF 程序可能需要锁定一定量的内存,因此必须调整这个限制。

2.准备 union bpf_attr 结构体:

union bpf_attr是一个内核接口,包含了加载 eBPF 程序所需的所有参数。函数开始时,代码将这个结构体初始化,并填充有关 eBPF 程序的详细信息,比如:

  • prog_type: eBPF 程序的类型(如 BPF_PROG_TYPE_XDPBPF_PROG_TYPE_TRACEPOINT)。

  • insns: 指向编译好的 eBPF 字节码的指针。

  • insn_cnt: eBPF 指令的数量。

  • license: eBPF 程序的许可证信息。

  • prog_flags: 可选的程序标志。

3.设置与附加点相关的字段:

  • 如果 eBPF 程序需要附加到某个特定的内核钩子点(如 tc 或 XDP),则会设置 attach_prog_fdattach_btf_obj_fd,这些字段告诉内核要将 eBPF 程序附加到哪。

4.调用 sys_bpf_prog_load:

  • 核心部分:加载 eBPF 程序的关键步骤是调用系统调用 sys_bpf_prog_load,这是直接与内核通信的接口。它将填充好的 union bpf_attr 传递给内核,内核验证并尝试加载程序。

  • sys_bpf_prog_load 函数会返回一个文件描述符(fd),表示成功加载的 eBPF 程序。如果失败,会返回一个负值,并设置 errno

接下来再往下追踪sys_bpf_prog_load函数,最终追踪到了系统调用层面。

static inline int sys_bpf(enum bpf_cmd cmd, union bpf_attr *attr,
 unsigned int size)
{
return syscall(__NR_bpf, cmd, attr, size);
}

总结一下,在加载阶段,会将编译阶段的eBPF字节码文件进行进一步的处理,并通过系统调用加入到内核空间中去 。

加载之后,内核就可以运行eBPF程序了吗?那当然不行,加载进内核的程序肯定是要安全可靠的,要是eBPF程序有问题,进而造成内核崩溃,那后果可太严重了。针对此问题,内核虚拟机里的验证器发挥了巨大作用。

三、验证阶段

验证阶段的工作非常繁琐,总之它的作用就是要验证你的程序对于内核来说是无害的。以下是内核中eBPF 验证器的主要入口函数代码:(/kernel/bpf/verifier.c)

int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr)
{
u64 start_time = ktime_get_ns(); // 获取验证器开始时间,用于计算验证时间
struct bpf_verifier_env *env; // 定义验证器环境
struct bpf_verifier_log *log; // 定义日志
int i, len, ret = -EINVAL; // 初始化一些变量
bool is_priv; // 检查用户是否拥有特权权限

// 为 bpf_verifier_env 分配内存。它包含了程序的验证状态和日志信息
env = kzalloc(sizeof(struct bpf_verifier_env), GFP_KERNEL);
if (!env)
return -ENOMEM; // 如果内存分配失败,返回错误
log = &env->log; // 初始化日志指针

len = (*prog)->len; // 获取程序的指令长度
// 分配内存给每条指令的辅助数据,保存原始索引信息
env->insn_aux_data = vzalloc(array_size(sizeof(struct bpf_insn_aux_data), len));
ret = -ENOMEM;
if (!env->insn_aux_data)
goto err_free_env; // 如果内存分配失败,跳转到清理部分
for (i = 0; i < len; i++)
env->insn_aux_data[i].orig_idx = i; // 初始化每条指令的原始索引
env->prog = *prog; // 将程序指针赋值给验证环境
env->fd_array = make_bpfptr(attr->fd_array, uattr.is_kernel); // 生成文件描述符数组指针
is_priv = bpf_capable(); // 检查是否具有执行 eBPF 的权限

// 初始化验证器状态
mark_verifier_state_clean(env);

// 处理 BPF_LD_IMM64 伪指令,这是将立即数加载到寄存器的特殊指令
ret = resolve_pseudo_ldimm64(env);
if (ret < 0)
goto skip_full_check; // 如果处理失败,跳转到后续部分

// 检查程序的控制流图,确保它是合法的
ret = check_cfg(env);
if (ret < 0)
goto skip_full_check; // 如果控制流检查失败,跳过后续的完整检查

// 验证子程序(如果有的话)模拟整个运行过程,特别是对于指针访问的每个细节进行检查
//!!!!!这里就是经常编写ebpf程序容易报错的地方
ret = do_check_subprogs(env);  
ret = ret ?: do_check_main(env); // 验证主程序

skip_full_check:
// 释放在验证期间分配的状态表内存
kvfree(env->explored_states);

// 优化程序中的循环结构
if (ret == 0)
ret = optimize_bpf_loop(env);

// 如果用户有特权,移除程序中的死代码
if (is_priv) {
if (ret == 0)
ret = opt_remove_dead_code(env);
} else {
// 如果没有特权,只清理死代码(不移除)
if (ret == 0)
sanitize_dead_code(env);
}

// 转换上下文中的特定访问,如对网络包字段的访问
if (ret == 0)
ret = convert_ctx_accesses(env);

// 修正函数调用参数,确保其合法性
if (ret == 0)
ret = fixup_call_args(env);  

// 记录验证所花费的总时间
env->verification_time = ktime_get_ns() - start_time;

err_free_env:
// 清理分配的验证器环境
kfree(env);
return ret;
}

这里列举的代码并不全面,读者要是想深入了解验证器的实现过程,请自行阅读源码。验证器所需要验证的内容和所需做的工作如下图所示:

四、JIT阶段

要是使用eBPF字节码再加上eBPF指令解释器来运行eBPF程序效率是很低的,JIT(即时编译,Just-In-Time compilation)很好的解决了这个问题,并且目前大多数机器支持了这个功能。使用JIT的优势如下:

  1. 通过 JIT,eBPF 字节码被直接编译成底层的机器代码,程序可以直接在 CPU 上执行,无需经过解释过程。因此,JIT 编译可以显著提高 eBPF 程序的执行速度,尤其是在高频调用或性能关键场景下。

  2. 解释器在逐条执行 eBPF 字节码时,需要进行大量的上下文切换和检查操作,增加了运行的开销。而 JIT 编译后的机器代码是直接在内核态执行的,这减少了指令解释、寄存器管理、栈管理等操作的开销。

  3. eBPF 常用于网络包处理、性能监控等高性能场景。JIT 编译后的程序执行速度接近于内核中的原生代码,非常适合这些需要低延迟、高吞吐量的任务。特别是像 XDP(eXpress Data Path)这种需要处理每一个网络包的应用场景中,JIT 编译极大地减少了延迟。

  4. 不同的 CPU 体系结构(如 x86、ARM)可以通过 JIT 编译生成最适合当前架构的机器代码,充分利用硬件资源,从而进一步提升运行效率。

接下来我们来查看一下eBPF字节码如何翻译成机器指令(x86体系结构),由于我们开始编写的bpftrace程序:bpftrace -e 'kprobe:do_nanosleep { printf("PID %d sleeping...\n", pid); }'使用到了pid变量,其实是调用了get_current_pid_tgid这个帮助函数,以此这里以call指令的翻译过程来举例:(arch/x86/net/bpf_jit_comp.c)

static int do_jit(struct bpf_prog *bpf_prog, int *addrs, u8 *image, u8 *rw_image,
 int oldproglen, struct jit_context *ctx, bool jmp_padding)
{
......
   case BPF_JMP | BPF_CALL: {
               int offs;

               func = (u8 *) __bpf_call_base + imm32;
               if (tail_call_reachable) {
                   RESTORE_TAIL_CALL_CNT(bpf_prog->aux->stack_depth);
                   if (!imm32)
                       return -EINVAL;
                   offs = 7 + x86_call_depth_emit_accounting(&prog, func);
              } else {
                   if (!imm32)
                       return -EINVAL;
                   offs = x86_call_depth_emit_accounting(&prog, func);
              }
               if (emit_call(&prog, func, image + addrs[i - 1] + offs))
                   return -EINVAL;
               break;
          }
......
}

通过分析,我们再追踪emit_call函数:

static int emit_call(u8 **pprog, void *func, void *ip)
{
return emit_patch(pprog, func, ip, 0xE8);
}

最终,call指令翻译到x86中的操作码是0xE8。

通过反汇编查看指令get_current_pid_tgid指令:

bpftool prog dump jited id 965

第11行是get_current_pid_tgid函数的汇编指令。

接下来我们通过bpftool工具来查看eBPF程序通过JIT翻译后的的机器码:

#这里的965是本机ebpf程序的编号
bpftool prog dump jited id 965 opcodes | grep -v :

可以发现,第十一行的call指令被翻译成了e8,正好与JIT翻译后的CALL指令的机器码一致。

五、总结

通过本文对 eBPF 原理的分析,希望能帮助大家在 eBPF 实现原理层面获得更深入的认识。虽然本文在某些细节上尚未进行深入探讨,但我们相信它能为您提供 eBPF 程序运行全过程的初步认知,并为您后续深入学习特定方面奠定基础。若文中存在理解不准确的地方,欢迎指正,感谢各位读者的耐心阅读。

参考资料:

  • 性能专家 Brendan Gregg(《性能之巅》作者)在 USENIX 上做的关于 eBPF 工作原理的演讲和相关文档
  • 《Linux内核观测技术BPF》译者之一,狄卫华老师关于eBPF原理讲解视频。
  • 书籍:《BPF之巅》

Linux内核之旅
Linux内核之旅
 最新文章