BPF执行原理
-
编译BPF程序:BPF程序可以使用C语言、BPF汇编语言或编译器插件进行编写。编写完成后,BPF程序会被编译为一段机器码,该机器码是一组低级指令,类似于虚拟机指令。
-
加载BPF程序:BPF程序通常被封装在一个BPF对象文件中,该文件包含了BPF程序的机器码、符号表、重定位表等元数据。使用libbpf库或其他BPF工具,可以将BPF对象文件加载到内核中。
-
验证BPF程序:在加载BPF程序之前,内核会对其进行验证。这个过程包括检查程序的合法性、查找和解析程序中的符号等。验证确保BPF程序不会破坏内核的稳定性和安全性。
-
JIT编译(可选):为了提高执行速度,BPF程序可以进行即时编译(JIT)。JIT编译器会将BPF程序的机器码转换为与当前CPU架构兼容的本地机器码。这样,BPF程序就可以直接在CPU上运行,而不需要解释执行。
-
执行BPF程序:一旦BPF程序被加载和编译,内核就可以执行该程序了。BPF程序通常与网络数据包处理相关,可以被挂钩到网络接口的不同阶段,如入站、出站、转发等。当网络数据包经过被挂钩的阶段时,内核会执行相应的BPF程序来过滤或处理数据包。
-
程序返回和结果处理:BPF程序执行完成后,会返回一个结果。这个结果可以是允许或拒绝数据包通过,也可以是对数据包进行修改或采集一些元数据等。内核会根据BPF程序的返回值来决定如何处理数据包。
BPF指令由Linux内核的BPF运行时模块执行,具体来说,该运行时模块提供两种执行机制:一个解释器和一个将BPF指令动态转换为本地化指令的即时编译器(JIT)。注意只有当BPF程序通过解释器执行时其实现才是一种虚拟机。当前JIT启用后BFP指令将通过JIT编译后像如何其它本地内核代码一样,直接在处理器上运行。如果没有启用JIT则经由解释器执行。相比于解释器执行,JIT执行的性能更好。一些发行版在x86架构上JIT是默认开启的,完全移除了内核中解释器的实现。
BPF Hooks
SEC关键字
在BPF中,SEC是一个用于指定BPF程序属性的关键字。SEC是Section的缩写,表示代码段或数据段。SEC关键字用于将BPF程序中的函数或变量放置在特定的代码段或数据段中,以便在加载和执行时进行控制。
BPF程序可以包含多个函数和变量,而SEC关键字用于对它们进行分组和分类。常用的SEC关键字包括:
-
SEC("kprobe/")
:用于将BPF程序挂钩到内核的kprobe(内核探针)上。kprobe允许在内核函数的入口点处执行自定义的BPF程序。 -
SEC("kretprobe/")
:用于将BPF程序挂钩到内核的kretprobe(内核返回探针)上。kretprobe允许在内核函数的返回点处执行自定义的BPF程序。 -
SEC("tracepoint/")
:用于将BPF程序挂钩到内核的tracepoint(跟踪点)上。tracepoint是内核中预定义的事件,可以用于跟踪内核的各种操作。 -
SEC("perf_event/")
:用于将BPF程序挂钩到内核的性能事件上。perf_event是内核中的性能事件接口,可以用于测量和跟踪系统的性能指标。 -
SEC(“tracepoint”):指定函数为跟踪点(tracepoint)。跟踪点是BPF程序中用于在Linux内核的跟踪事件中插入代码的部分。
-
SEC(“maps”):指定变量为BPF映射(maps)。BPF映射是一种数据结构,用于在BPF程序和用户空间之间共享数据。
-
SEC(“license”):指定变量为BPF程序的许可证(license)。许可证用于说明BPF程序的使用权限和限制。
-
SEC("xdp_sock")
是一个特殊的BPF宏,用于指定一个BPF程序的类型为XDP(eXpress Data Path)套接字型。在BPF程序中使用SEC("xdp_sock")
宏,可以将该程序挂钩到Linux内核中的XDP处理路径上
BPF映射
在eBPF(extended Berkeley Packet Filter)中,映射(Map)是一种用于在用户空间和内核空间之间共享数据的机制。映射可以看作是一种键-值存储结构,用于在eBPF程序执行期间读取和写入数据。
映射提供了一种将数据从用户空间传递到内核空间的方式,以及将数据从内核空间传递回用户空间的方式。映射可以从用户空间被读取和修改,并且在eBPF程序执行期间可以在内核空间中进行读取和修改。
所有 map 类型的定义:
enum bpf_map_type {
BPF_MAP_TYPE_UNSPEC,
BPF_MAP_TYPE_HASH,
BPF_MAP_TYPE_ARRAY,
BPF_MAP_TYPE_PROG_ARRAY,
BPF_MAP_TYPE_PERF_EVENT_ARRAY,
BPF_MAP_TYPE_PERCPU_HASH,
BPF_MAP_TYPE_PERCPU_ARRAY,
BPF_MAP_TYPE_STACK_TRACE,
BPF_MAP_TYPE_CGROUP_ARRAY,
BPF_MAP_TYPE_LRU_HASH,
BPF_MAP_TYPE_LRU_PERCPU_HASH,
BPF_MAP_TYPE_LPM_TRIE,
BPF_MAP_TYPE_ARRAY_OF_MAPS,
BPF_MAP_TYPE_HASH_OF_MAPS,
BPF_MAP_TYPE_DEVMAP,
BPF_MAP_TYPE_SOCKMAP,
BPF_MAP_TYPE_CPUMAP,
BPF_MAP_TYPE_XSKMAP,
BPF_MAP_TYPE_SOCKHASH,
BPF_MAP_TYPE_CGROUP_STORAGE,
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
BPF_MAP_TYPE_QUEUE,
BPF_MAP_TYPE_STACK,
BPF_MAP_TYPE_SK_STORAGE,
BPF_MAP_TYPE_DEVMAP_HASH,
BPF_MAP_TYPE_STRUCT_OPS,
BPF_MAP_TYPE_RINGBUF,
BPF_MAP_TYPE_INODE_STORAGE,
};
创建BPF maps
BPF_MAP_TYPE_ARRAY类型的map创建
struct {
//指定了映射的类型为数组。BPF_MAP_TYPE_ARRAY是一个预定义的宏,用于表示数组映射。
__uint(type, BPF_MAP_TYPE_ARRAY);
//指定了映射键的类型为u32。
__type(key, u32);
//指定了映射值的类型为long
__type(value, long);
//指定了映射的最大条目数为256,即映射中可以存储的最大元素数量
__uint(max_entries, 256);
//指定了映射应该被放置在特定的eBPF映射节中。这是为了在加载eBPF程序时将映射与相应的节关联起来
} my_map SEC(".maps");
BPF_MAP_TYPE_HASH类型的map创建
struct pair {
long packets;
long bytes;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH); // BPF map 类型
__type(key, __be32); // 目的 IP 地址
__type(value, struct pair); // 包数和字节数
__uint(max_entries, 1024); // 最大 entry 数量
} hash_map SEC(".maps");
SEC(".maps")
和bpf_create_map
都是用于创建eBPF映射(Map)的机制,但它们的原理和实现方式略有不同。
-
SEC(".maps")
原理:SEC(".maps")
是一个编译器指示,用于在eBPF程序中定义映射的结构和初始化数据。在编译eBPF程序时,编译器会将使用SEC(".maps")
指示定义的映射结构和初始化数据放置在特定的程序节(Section)中。当eBPF程序加载到内核时,内核会解析程序节,并根据SEC(".maps")
指示找到映射的定义和初始化数据。然后,内核根据这些信息来创建映射,并将其挂载到内核的映射表中。这种方式适用于在编译时确定映射结构和数据的情况。 -
bpf_create_map
原理:bpf_create_map
是一个运行时的系统调用,用于在eBPF程序运行时动态创建映射。当程序调用bpf_create_map
系统调用时,内核会根据传递的映射类型和配置参数,创建一个新的映射对象。映射对象在内核中分配内存,并初始化相关数据结构。然后,内核会返回一个映射的文件描述符(File Descriptor)给程序,程序可以通过该文件描述符来对映射进行操作。这种方式适用于需要在程序运行时根据需求动态创建映射的情况。
更新元素
bpf_map_update_elem
是eBPF中的一个函数,用于在eBPF程序中更新映射中的元素或插入新的元素
int bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags);
-
map
是一个指向映射的指针,表示要进行更新或插入操作的映射。 -
key
是一个指向要更新或插入的元素的键的指针。 -
value
是一个指向要更新或插入的元素的值的指针。 -
flags
是一个用于指定更新操作的标志位。可以使用BPF_NOEXIST
标志表示仅在键不存在时进行插入操作,使用BPF_EXIST
标志表示仅在键已存在时进行更新操作。 -
该函数返回一个整数,表示操作的结果。如果操作成功,则返回0;如果操作失败,则返回一个负数错误代码。
读取元素
bpf_map_lookup_elem
是eBPF中的一个函数,用于在eBPF程序中查找映射中的元素
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key);
-
map
是一个指向映射的指针,表示要进行查找的映射。 -
key
是一个指向要查找的元素的键的指针。 -
该函数返回一个
void
指针,指向映射中与给定键匹配的元素的值。如果找到匹配的元素,则返回对应的值的指针;如果未找到匹配的元素,则返回NULL。
迭代遍历元素和查找
bpf_map_lookup_elem
是eBPF中的一个函数,用于在eBPF程序中查找映射中的元素。
该函数的原型如下:
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key);
-
map
是一个指向映射的指针,表示要进行查找的映射。 -
key
是一个指向要查找的元素的键的指针。
该函数返回一个void
指针,指向映射中与给定键匹配的元素的值。如果找到匹配的元素,则返回对应的值的指针;如果未找到匹配的元素,则返回NULL。
bpf_map_get_next_key
是eBPF中的一个函数,用于在eBPF程序中获取映射中下一个键的值。
该函数的原型如下:
int bpf_map_get_next_key(struct bpf_map *map, const void *key, void *next_key);
-
map
是一个指向映射的指针,表示要进行操作的映射。 -
key
是一个指向当前键的指针,表示要获取下一个键的值。 -
next_key
是一个指向存储下一个键的值的缓冲区的指针。
该函数返回一个整数,表示操作的结果。如果成功获取到下一个键的值,则返回0;如果没有下一个键或发生其他错误,则返回一个负数错误代码。
删除
bpf_map_delete_elem
是eBPF中的一个函数,用于在eBPF程序中删除映射中的元素。
该函数的原型如下:
int bpf_map_delete_elem(struct bpf_map *map, const void *key);
-
map
是一个指向映射的指针,表示要进行操作的映射。 -
key
是一个指向要删除的元素的键的指针。
该函数返回一个整数,表示操作的结果。如果成功删除元素,则返回0;如果未找到匹配的元素或发生其他错误,则返回一个负数错误代码。
案例1
内核态代码
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2017 Facebook
*/
#include <uapi/linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct syscalls_enter_open_args {
unsigned long long unused;
long syscall_nr;
long filename_ptr;
long flags;
long mode;
};
struct syscalls_exit_open_args {
unsigned long long unused;
long syscall_nr;
long ret;
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} enter_open_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} exit_open_map SEC(".maps");
static __always_inline void count(void *map)
{
u32 key = 0;
u32 *value, init_val = 1;
value = bpf_map_lookup_elem(map, &key);
if (value)
*value += 1;
else
bpf_map_update_elem(map, &key, &init_val, BPF_NOEXIST);
}
SEC("tracepoint/syscalls/sys_enter_open")
int trace_enter_open(struct syscalls_enter_open_args *ctx)
{
count(&enter_open_map);
return 0;
}
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_enter_open_at(struct syscalls_enter_open_args *ctx)
{
count(&enter_open_map);
return 0;
}
SEC("tracepoint/syscalls/sys_exit_open")
int trace_enter_exit(struct syscalls_exit_open_args *ctx)
{
count(&exit_open_map);
return 0;
}
SEC("tracepoint/syscalls/sys_exit_openat")
int trace_enter_exit_at(struct syscalls_exit_open_args *ctx)
{
count(&exit_open_map);
return 0;
}
用户态代码
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2017 Facebook
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <linux/perf_event.h>
#include <errno.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
/* This program verifies bpf attachment to tracepoint sys_enter_* and sys_exit_*.
* This requires kernel CONFIG_FTRACE_SYSCALLS to be set.
*/
static void usage(const char *cmd)
{
printf("USAGE: %s [-i num_progs] [-h]\n", cmd);
printf(" -i num_progs # number of progs of the test\n");
printf(" -h # help\n");
}
static void verify_map(int map_id,const char* name)
{
__u32 key = 0;
__u32 val;
if (bpf_map_lookup_elem(map_id, &key, &val) != 0) {
fprintf(stderr, "map_lookup failed: %s\n", strerror(errno));
return;
}
if (val == 0) {
fprintf(stderr, "failed: map #%d returns value 0\n", map_id);
return;
}
printf("name=%s, var=%d\n",name,val);
val = 0;
if (bpf_map_update_elem(map_id, &key, &val, BPF_ANY) != 0) {
fprintf(stderr, "map_update failed: %s\n", strerror(errno));
return;
}
}
static int test(char *filename, int num_progs)
{
int map0_fds[num_progs], map1_fds[num_progs], fd, i, j = 0;
struct bpf_link *links[num_progs * 4];
struct bpf_object *objs[num_progs];
struct bpf_program *prog;
for (i = 0; i < num_progs; i++) {
objs[i] = bpf_object__open_file(filename, NULL);
if (libbpf_get_error(objs[i])) {
fprintf(stderr, "opening BPF object file failed\n");
objs[i] = NULL;
goto cleanup;
}
/* load BPF program */
if (bpf_object__load(objs[i])) {
fprintf(stderr, "loading BPF object file failed\n");
goto cleanup;
}
map0_fds[i] = bpf_object__find_map_fd_by_name(objs[i],
"enter_open_map");
map1_fds[i] = bpf_object__find_map_fd_by_name(objs[i],
"exit_open_map");
if (map0_fds[i] < 0 || map1_fds[i] < 0) {
fprintf(stderr, "finding a map in obj file failed\n");
goto cleanup;
}
bpf_object__for_each_program(prog, objs[i]) {
links[j] = bpf_program__attach(prog);
if (libbpf_get_error(links[j])) {
fprintf(stderr, "bpf_program__attach failed\n");
links[j] = NULL;
goto cleanup;
}
j++;
}
printf("prog #%d: map ids %d %d\n", i, map0_fds[i], map1_fds[i]);
}
/* current load_bpf_file has perf_event_open default pid = -1
* and cpu = 0, which permits attached bpf execution on
* all cpus for all pid's. bpf program execution ignores
* cpu affinity.
*/
/* trigger some "open" operations */
fd = open(filename, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "open failed: %s\n", strerror(errno));
return 1;
}
close(fd);
/* verify the map */
for (i = 0; i < num_progs; i++) {
verify_map(map0_fds[i],"enter_open_map");
verify_map(map1_fds[i],"exit_open_map");
}
cleanup:
for (j--; j >= 0; j--)
bpf_link__destroy(links[j]);
for (i--; i >= 0; i--)
bpf_object__close(objs[i]);
return 0;
}
int main(int argc, char **argv)
{
struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
int opt, num_progs = 1;
char filename[256];
while ((opt = getopt(argc, argv, "i:h")) != -1) {
switch (opt) {
case 'i':
num_progs = atoi(optarg);
break;
case 'h':
default:
usage(argv[0]);
return 0;
}
}
setrlimit(RLIMIT_MEMLOCK, &r);
snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
return test(filename, num_progs);
}