本文将介绍如何在 ebpf/libbpf 程序中使用 eBPF 的尾调用(tail calls)特性。
尾调用(tail calls)¶
eBPF 的尾调用(tail calls)特性允许一个 eBPF 程序可以调用另一个 eBPF 程序, 并且调用完成后不会返回原来的程序。 因为尾调用在调用函数的时候会重用调用方函数的 stack frame,所以它的开销比普通的函数 调用会更低。
图片来源:https://docs.cilium.io/en/v1.12/bpf/#tail-calls
尾调用涉及两个步骤:
- 定义一个类型为 BPF_MAP_TYPE_PROG_ARRAY 的 map , map 的 value 是在尾调用中被调用的 eBPF 程序的文件描述符。 我们可以在用户态程序中更新这个 map 的 key/value。
- 在 eBPF 程序中,我们可以通过 bpf_tail_call() 这个辅助函数 从第1步的 map 中获取 eBPF 程序然后执行该程序进行尾调用。
使用示例¶
如前面所说,要使用尾调用特性我们需要定义一个 map 以及在 eBPF 程序中使用辅助函数执行尾调用。下面将以示例的代码的方式讲述每个步骤的关键代码。
定义 BPF_MAP_TYPE_PROG_ARRAY 类型的 map¶
可以通过下面的方法定义一个 BPF_MAP_TYPE_PROG_ARRAY 类型的 map:
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
__uint(max_entries, 1024);
} tail_jmp_map SEC(".maps");
如果想要在定义这个 map 的时候初始化一些值的话,可以用下面的方法:
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
__uint(max_entries, 1024);
__array(values, int (void *)); // 这个 values 必须有
} tail_jmp_map SEC(".maps") = {
.values = { // 初始化一些值
[268] = (void *)&enter_fchmodat,
},
};
用户态更新 map¶
在用户态程序中可以通过 bpf_map_update_elem 函数更新这个 map:
tail_jump_map_fd = bpf_object__find_map_fd_by_name(bpf_obj, "tail_jmp_map");
bpf_map_update_elem(tail_jump_map_fd, &key, &bpf_program_fd, BPF_ANY);
尾调用¶
eBPF 程序中可以通过 bpf_tail_call 辅助函数执行尾调用:
SEC("raw_tracepoint/sys_enter")
int raw_tracepoint__sys_enter(struct bpf_raw_tracepoint_args *ctx) {
u32 syscall_id = ctx->args[1];
// 执行尾调用
bpf_tail_call(ctx, &tail_jmp_map, syscall_id);
// 如果在 map 中找不到对应的 ebpf 程序的话,会继续走到后面的代码
char fmt[] = "no bpf program for syscall %d\n";
bpf_trace_printk(fmt, sizeof(fmt), syscall_id);
return 0;
}
完整的示例程序,详见: https://github.com/mozillazg/hello-libbpfgo/tree/master/22-tail-calls
Comments