注意:在4.17 内核中,Syscall 函数发生了重命名。从4.17 版本开始,用于Syscall krpobe调用的sys_kill对应当前的__x64_sys_kill(在x64系统上,不同的架构具有不同的前缀)。在附加一个kprobe/kretprobe时应该注意这一点。但如果可能的话,尽可能遵循tracepoints。
如果要开发一个新的,带tracepoint/kprobe/kretprobe的BPF程序,查看新的raw_tp/fentry/fexit 探针,它们提供了更好的性能和易用性(内核5.5开始提供此功能)。
在BCC中处理编译时的#if在BCC模式中大量使用了预处理#ifdef 和 #if 条件。大部分是因为支持不同的内核版本或启用/禁用可选择的逻辑(依赖应用配置)。此外,BCC允许在用户空间侧提供自定义的#define,在BPF代码编译期间的运行时阶段进行替换。通常用于自定义各种参数。
不能使用libbpf + BPF CO-RE做类似的事情(通过编译时(compile-time)逻辑),原因是BPF程序遵循一次编译就可以在所有可能的内核以及应用配置上运行。
为了处理不同的内核版本,BPF CO-RE支持两种补充机制:Kconfig externs 和 struct “flavors”(在上一篇博客中有涉及)。通过声明外部变量,BPF代码可以知道处理的内核版本:
#define KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + (c)) extern int LINUX_KERNEL_VERSION __kconfig; if (LINUX_KERNEL_VERSION < KERNEL_VERSION(5, 2, 0)) { /* deal with older kernels */ } else { /* 5.2 or newer */ }类似地,可以通过从Kconfig(位于内核的.config文件中)中抽取类似CONFIG_xxx的变量来获取内核版本:
extern int CONFIG_HZ __kconfig; /* now you can use CONFIG_HZ in calculations */通常,如果重命名了一个字段,或将其移入一个子结构体中时,可以通过检查目标内核是否存在该字段来判断是否发生了这种情况。可以通过bpf_core_field_exists(<field>)实现,如果返回1,则表示目标字段位于目标内核中;返回0则表示不存在内核中。配合struct flavors,可以处理内核结构布局的发生重大变动的情况。下面是一个简短的例子,展示了如何适应 struct kernfs_iattrs在不同内核版本中的变化:
/* struct kernfs_iattrs will come from vmlinux.h */ struct kernfs_iattrs___old { struct iattr ia_iattr; }; if (bpf_core_field_exists(root_kernfs->iattr->ia_mtime)) { data->cgroup_root_mtime = BPF_CORE_READ(root_kernfs, iattr, ia_mtime.tv_nsec); } else { struct kernfs_iattrs___old *root_iattr = (void *)BPF_CORE_READ(root_kernfs, iattr); data->cgroup_root_mtime = BPF_CORE_READ(root_iattr, ia_iattr.ia_mtime.tv_nsec); } 应用配置BPF CO-RE的办法是使用全局变量自定义程序的行为。全局变量允许用户空间app在BPF程序加载和校验前预配置必要的参数和标志。全局变量可以是可变的或恒定的。常量(只读)最常用于指定一个BPF程序的一次性配置(在程序加载和校验前)。可变的量在BPF程序加载并运行后,可用于BPF程序与其用户空间副本之间的双向数据交换。
在BPF代码侧,可以使用一个const volatile全局变量(当用于可变的量时,只需丢弃const volatile修饰符)声明只读的全局变量。
const volatile struct { bool feature_enabled; int pid_to_filter; } my_cfg = {};有如下几点需要重点关注:
必须指定const volatile来防止不合时宜的编译器优化(编译器可能并且会错误地采用零值并将其内联到代码中);
如果定义了一个可变的(非const)量时,确保不会被标记为static:非静态全局变量最好与编译器配合。这种情况下通常不需要volatile。
变量需要被初始化,否则libbpf会拒绝加载BPF应用。初始值可以为0或其他任意值。这类值作为变量的默认值,除非在控制应用程序中覆盖。
使用BPF代码中的全局变量很简单:
if (my_cfg.feature_enabled) { /* … */ } if (my_cfg.pid_to_filter && pid == my_cfg.pid_to_filter) { /* … */ }全局变量提供了更好的用户体验,并避免了BPF map查询造成的开销。此外,对于不变的量,它们的值是对BPF验证器来说是透明的(众所周知的),并在程序验证期间将其视为常量。这种方式可以允许BPF校验器精确且高效地消除无用代码分支。