在 ebpf/libbpf 程序中使用尾调用(tail calls)

本文将介绍如何在 ebpf/libbpf 程序中使用 eBPF 的尾调用(tail calls)特性。

尾调用(tail calls)

eBPF 的尾调用(tail calls)特性允许一个 eBPF 程序可以调用另一个 eBPF 程序, 并且调用完成后不会返回原来的程序。 因为尾调用在调用函数的时候会重用调用方函数的 stack frame,所以它的开销比普通的函数 调用会更低。

image

图片来源: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