eBPF笔记(六)——The bpf() System Call

The bpf() System Call

正如你在第1章中看到的,当用户空间应用程序希望内核代表它们执行某些操作时,它们会使用系统调用API发出请求。因此,如果一个用户空间应用程序想要将一个eBPF程序加载到内核中,必然涉及一些系统调用。实际上,有一个名为bpf()的系统调用,在本章中我将向你展示如何使用它来加载和与eBPF程序和map交互。

值得注意的是,运行在内核中的eBPF代码不使用syscalls来访问map。系统调用接口仅由用户空间应用程序使用。相反,eBPF程序使用辅助函数来读取和写入map;你在前两章中已经看到了这方面的示例。

如果你继续自己编写eBPF程序,很可能你不会直接调用这些bpf()系统调用。本书后面会讨论的库提供了更高级别的抽象,使事情变得更容易。尽管如此,这些抽象通常与你在本章中看到的底层系统调用命令相对应。无论你使用哪个库,你都需要掌握底层操作——加载程序,创建和访问map等——你将在本章中看到的操作。

在我向你展示bpf()系统调用的示例之前,让我们考虑一下bpf()的man页上说的内容,即bpf()用于“对扩展BPF map或程序执行命令”。它还告诉我们,bpf()的签名如下:
int bpf(int cmd, union bpf_attr *attr, unsigned int size);

bpf()的第一个参数cmd指定要执行的命令。bpf()系统调用不只是做一件事——有很多不同的命令可以用来操作eBPF程序和map。图4-1显示了用户空间代码可能使用的一些常见命令的概述,用于加载eBPF程序、创建map、将程序附加到事件,并访问map中的键值对。

bpf()系统调用的attr参数保存着指定命令所需的任何数据,而size指示attr中有多少字节的数据。

在这个例子中,我将使用一个名为hello-buffer-config.py的BCC程序,它在第2章中看到的示例基础上进行了扩展。与hello-buffer.py示例类似,这个程序在每次运行时都向perf缓冲区发送一条消息,传递有关execve()系统调用事件的内核到用户空间的信息。这个版本的新功能是允许为每个用户ID配置不同的消息。

点击查看完整代码
#!/usr/bin/python3  
# -*- coding: utf-8 -*-
from bcc import BPF
import ctypes as ct

program = r"""
struct user_msg_t {
   char message[12];
};

BPF_HASH(config, u32, struct user_msg_t);

BPF_PERF_OUTPUT(output); 

struct data_t {     
   int pid;
   int uid;
   char command[16];
   char message[12];
};

int hello(void *ctx) {
   struct data_t data = {}; 
   struct user_msg_t *p;
   char message[12] = "Hello World";

   data.pid = bpf_get_current_pid_tgid() >> 32;
   data.uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;

   bpf_get_current_comm(&data.command, sizeof(data.command));

   p = config.lookup(&data.uid);
   if (p != 0) {
      bpf_probe_read_kernel(&data.message, sizeof(data.message), p->message);       
   } else {
      bpf_probe_read_kernel(&data.message, sizeof(data.message), message); 
   }

   output.perf_submit(ctx, &data, sizeof(data)); 
 
   return 0;
}
"""

b = BPF(text=program) 
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")
b["config"][ct.c_int(0)] = ct.create_string_buffer(b"Hey root!")
b["config"][ct.c_int(501)] = ct.create_string_buffer(b"Hi user 501!")
 
def print_event(cpu, data, size):  
   data = b["output"].event(data)
   print(f"{data.pid} {data.uid} {data.command.decode()} {data.message.decode()}")
 
b["output"].open_perf_buffer(print_event) 
while True:   
   b.perf_buffer_poll()


$ strace -e bpf ./hello-buffer-config.py
bpf(BPF_BTF_LOAD, {btf="\237\353\1\0\30\0\0\0\0\0\0\0\364\5\0\0\364\5\0\0#\v\0\0\1\0\0\0\0\0\0\10"..., btf_log_buf=NULL, btf_size=4399, btf_log_size=0, btf_log_level=0}, 128) = 3
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_PERF_EVENT_ARRAY, key_size=4, value_size=4, max_entries=128, map_flags=0, inner_map_fd=0, map_name="output", map_ifindex=0, btf_fd=0, btf_key_type_id=0, btf_value_type_id=0, btf_vmlinux_value_type_id=0, map_extra=0}, 128) = 4
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=4, value_size=12, max_entries=10240, map_flags=0, inner_map_fd=0, map_name="config", map_ifindex=0, btf_fd=3, btf_key_type_id=1, btf_value_type_id=4, btf_vmlinux_value_type_id=0, map_extra=0}, 128) = 5
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_KPROBE, insn_cnt=44, insns=0x7f33e9468be8, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(6, 5, 13), prog_flags=0, prog_name="hello", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS, prog_btf_fd=3, func_info_rec_size=8, func_info=0x5770adbd6a20, func_info_cnt=1, line_info_rec_size=16, line_info=0x5770ac5d9040, line_info_cnt=21, attach_btf_id=0, attach_prog_fd=0, fd_array=NULL}, 128) = 6
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=5, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=5, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0
.....

Loading BTF Data

对于bpf()的第一次调用有:
bpf(BPF_BTF_LOAD, {btf="\237\353\1\0...}, 128) = 3

这次对bpf()的调用正在将一块BTF数据加载到内核中,并且bpf()系统调用的返回代码(在我的例子中是3)是一个指向该数据的文件描述符。

Creating Maps

下一个 bpf() 创建输出 perf 缓冲区映射
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_PERF_EVENT_ARRAY, key_size=4, value_size=4,

从命令名称BPF_MAP_CREATE可以猜到,这个调用创建了一个eBPF map。你可以看到这个map的类型是PERF_EVENT_ARRAY,名为output。这个perf事件map中的键和值都是4个字节长。此map可以容纳的键值对数量有一个限制,由max_entries字段定义为四对。我稍后会解释为什么这个map中有四个条目。返回值4是用户空间代码访问output map的文件描述符。

输出中的下一个bpf()系统调用创建了config map:


bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=4, value_size=12, max_entries=10240, map_flags=0, inner_map_fd=0, map_name="config", map_ifindex=0, btf_fd=3, 
btf_key_type_id=1,btf_value_type_id=4, btf_vmlinux_value_type_id=0, map_extra=0}, 128) = 5

这个map被定义为一个哈希表map,键的长度为4个字节(对应于一个32位整数,可以用来保存用户ID),值的长度为12个字节(与msg_t结构的长度相匹配)。我没有指定表的大小,所以它被赋予了BCC的默认大小,即10240个条目。这个bpf()系统调用也返回了一个文件描述符,即5,用于在将来的系统调用中引用这个config map。
你还可以看到字段btf_fd=3,它告诉内核使用之前获取的BTF文件描述符3。正如你将在第5章中看到的,BTF信息描述了数据结构的布局,将其包含在map的定义中意味着有关于该map中使用的键和值类型布局的信息。这些信息被像bpftool这样的工具用于漂亮地打印map转储,使它们更具可读性——你在第3章中已经看到了一个示例

Loading a Program

bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_KPROBE, insn_cnt=44, insns=0x78bfd9e09be8, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(6, 5, 13), prog_flags=0, prog_name="hello", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS, prog_btf_fd=3, func_info_rec_size=8, func_info=0x5e16c4c71040, func_info_cnt=1, line_info_rec_size=16, line_info=0x5e16c2fb3d90, line_info_cnt=21, attach_btf_id=0, attach_prog_fd=0, fd_array=NULL}, 128) = 6

  • prog_type字段描述了程序类型,这里表明它是要附加到一个kprobe上的。你将在第7章中了解更多关于程序类型的信息。
  • insn_cnt字段表示“指令计数”,即程序中的字节码指令数量。
  • 组成这个eBPF程序的字节码指令存储在insns字段指定的内存地址中。
  • 该程序被指定为GPL许可证,以便可以使用GPL许可证的BPF辅助函数。
  • 程序名称为hello。
  • 虽然expected_attach_type字段的值为BPF_CGROUP_INET_INGRESS,这听起来像是与入口网络流量有关的内>容,但你知道这个eBPF程序将附加到一个kprobe上。实际上,expected_attach_type字段仅用于某些程序类型,而BPF_PROG_TYPE_KPROBE并不是其中之一。BPF_CGROUP_INET_INGRESS只是BPF附加类型列表中的第一个,所以它的值为0。
  • prog_btf_fd字段告诉内核要与该程序一起使用哪个先前加载的BTF数据块。这里的值3对应于你在BPF_BTF_LOAD系统调用中看到的返回的文件描述符(它与用于config map的BTF数据块相同)。

Modifying a Map from User Space

b["config"][ct.c_int(0)] = ct.create_string_buffer(b"Hey root!")
b["config"][ct.c_int(501)] = ct.create_string_buffer(b"Hi user 501!")

对于的bpf()调用是:

bpf(BPF_MAP_UPDATE_ELEM, {map_fd=5, key=0xffffa7842490, value=0xffffa7a2b410,
 flags=BPF_ANY}, 128) = 0

简单明了。
BPF_MAP_UPDATE_ELEM命令用于更新map中的键值对。BPF_ANY标志表示如果该键在map中不存在,则应创建它。这里有两个这样的调用,对应于为两个不同用户ID配置的两个条目

root@wp-virtual-machine:~# bpftool map dump name config
[{
        "key": 501,
        "value": {
            "message": [72,105,32,117,115,101,114,32,53,48,49,33
            ]
        }
    },{
        "key": 0,
        "value": {
            "message": "Hey root!"
        }
    }
]

bpftool如何知道如何格式化这个输出呢?例如,它如何知道值是一个结构体,包含一个名为message的字段,该字段包含一个字符串?答案是它使用了在定义这个map的BPF_MAP_CREATE系统调用中包含的BTF信息中的定义。你将在下一章中看到BTF如何传递这些信息的更多细节。
现在你已经看到了用户空间如何与内核交互以加载程序和map,并更新map中的信息。在你看到的系统调用序列中,程序还没有被附加到一个事件上。这一步必须进行,否则程序将永远不会被触发。

提醒一下:不同类型的eBPF程序以各种不同的方式附加到不同的事件上!本章后面我会向你展示这个示例中用于附加到kprobe事件的系统调用,而在这种情况下不涉及bpf()。相反,在本章末尾的练习中,我将向你展示另一个示例,其中使用bpf()系统调用将程序附加到原始跟踪点事件上。

在我们讨论这些细节之前,我想讨论一下当你停止运行程序时会发生什么。你会发现程序和map会自动卸载,这是因为内核使用引用计数来跟踪它们。

BPF Program and Map References

你知道,通过bpf()系统调用将BPF程序加载到内核中会返回一个文件描述符。在内核中,这个文件描述符是对该程序的引用。进行系统调用的用户空间进程拥有这个文件描述符;当该进程退出时,文件描述符被释放,程序的引用计数减少。当BPF程序没有引用时,内核会移除该程序。

当你将一个程序pin到文件系统时,会创建一个额外的引用。

Pinning

在之前不使用python加载而是手动加载ebpf程序中使用到了:
bpftool prog load hello.bpf.o /sys/fs/bpf/hello

这些固定的对象并不是持久化到磁盘上的真实文件。它们是在一个伪文件系统上创建的,该文件系统表现得像一个基于磁盘的常规文件系统,具有目录和文件。但它们实际上是保存在内存中的,这意味着它们在系统重启后不会保留

如果bpftool允许你加载程序而不固定它,那将毫无意义,因为当bpftool退出时,文件描述符会被释放,如果引用计数为零,程序将被删除,因此不会实现任何有用的效果。但是,将程序固定到文件系统意味着程序有一个额外的引用,因此在命令完成后程序仍然保持加载状态。

当BPF程序附加到将触发它的钩子时,引用计数也会增加。这些引用计数的行为取决于BPF程序类型。你将在第7章中了解更多关于这些程序类型的信息,但有些类型与追踪(如kprobes和tracepoints)相关,并且总是与用户空间进程相关联;对于这些类型的eBPF程序,当该进程退出时,内核的引用计数会减少。附加在网络栈或控制组(cgroups,简称“控制组”)中的程序则与任何用户空间进程无关,因此即使加载它们的用户空间程序退出,它们仍然保持有效。你已经在使用ip link命令加载XDP程序时看到过这种情况:

wp@wp-virtual-machine:~/projects/learning-ebpf/chapter3$ sudo ip link set dev ens33 xdp obj hello.bp
f.o sec xdp

wp@wp-virtual-machine:~/projects/learning-ebpf/chapter3$ sudo bpftool prog list
...
253: xdp  name hello  tag d35b94b4c0c10efb  gpl
        loaded_at 2024-05-22T20:31:41+0800  uid 0
        xlated 96B  jited 64B  memlock 4096B  map_ids 71,70
        btf_id 218

这个程序的引用计数非零,因为在ip link命令完成后,该程序仍然附加在XDP钩子上。

eBPF映射(maps)也有引用计数,当引用计数降至零时,这些映射会被清理。每个使用映射的eBPF程序都会增加映射的引用计数,用户空间程序持有映射的文件描述符也会增加引用计数。

有时eBPF程序的源代码中可能定义了一个程序实际并不引用的映射。假设你想存储一些关于程序的元数据,你可以将其定义为一个全局变量,正如你在前一章所见,这些信息被存储在一个映射中。如果eBPF程序不操作这个映射,程序不会自动增加对该映射的引用计数。此时,可以使用BPF(BPF_PROG_BIND_MAP)系统调用将映射与程序关联,这样在用户空间加载程序退出且不再持有文件描述符引用映射时,该映射不会被清理。

映射也可以固定到文件系统上,用户空间程序可以通过知道映射的路径来访问这些映射。

另一种创建对BPF程序引用的方法是使用BPF链接(BPF link)。

BPF 链接提供了一个抽象层,介于 eBPF 程序和它附加的事件之间。BPF 链接本身可以固定到文件系统上,这会为程序创建一个额外的引用。这意味着将程序加载到内核中的用户空间进程可以终止,但程序仍然会保持加载状态。用户空间加载程序的文件描述符会被释放,减少对程序的引用计数,但由于 BPF 链接的存在,引用计数不会变为零。

如果你按照本章末尾的练习操作,你将有机会看到 BPF 链接的实际应用。现在,让我们回到 hello-buffer-config.py 使用的 bpf() 系统调用序列

Additional Syscalls Involved in eBPF

回顾一下,到目前为止,你已经看到了通过 bpf() 系统调用将 BTF 数据、程序和映射,以及映射数据添加到内核中的过程。接下来,strace 输出显示的内容与设置 perf 缓冲区有关

本章的其余部分将深入探讨使用 perf 缓冲区、环形缓冲区、kprobes 和映射迭代时涉及的系统调用序列。并不是所有的 eBPF 程序都需要执行这些操作,所以如果你赶时间或者觉得内容过于详细,可以直接跳到本章总结部分。我不会介意的!

Initializing the Perf Buffer

你已经看到了 bpf(BPF_MAP_UPDATE_ELEM) 调用,这些调用向配置映射添加条目。接下来,输出显示了一些类似这样的调用:
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=4, key=0x7f33e52f9a10, value=0x7f33e8486a90, flags=BPF_ANY}, 128) = 0

这些调用看起来与定义配置映射条目的调用非常相似,不同之处在于,此处的映射文件描述符是4,代表输出的 perf 缓冲区映射。和之前一样,键和值都是指针,所以从这个 strace 输出中无法知道键或值的数值。我看到这个 bpf() 系统调用重复了四次,所有参数的值都相同,不过没有办法知道这些指针所持有的值在每次调用之间是否发生了变化。查看这些 BPF_MAP_UPDATE_ELEM 调用会留下关于如何设置和使用缓冲区的一些未解问题。

  • 为什么有四次调用 BPF_MAP_UPDATE_ELEM?这是否与输出映射的最大条目数为四有关?

  • 在这四个 BPF_MAP_UPDATE_ELEM 实例之后,strace 输出中没有出现更多的 bpf() 系统调用。这似乎有点奇怪,因为映射的存在是为了让 eBPF 程序每次触发时写入数据,而且你已经看到用户空间代码显示了数据。很明显,这些数据不是通过 bpf() 系统调用从映射中检索的,那么它是如何获取的呢?

你还没有看到任何证据表明 eBPF 程序是如何附加到触发它的 kprobe 事件上的。为了解释这些疑问,我需要让 strace 显示在运行这个示例时的更多系统调用,如下所示。.
$ strace -e bpf,perf_event_open,ioctl,ppoll ./hello-buffer-config.py

为简洁起见,我将忽略与本示例的 eBPF 功能无关的 ioctl() 调用。

Attaching to Kprobe Events

posted @ 2025-07-26 15:31  Darkexpeller  阅读(49)  评论(0)    收藏  举报