eBPF笔记(五)—— eBPF Function Calls

Function Calls

    在早期,eBPF 程序不允许调用 helper function 以外的函数。为解决这个问题,可以考虑使用“always inline”
static __always_inline void my_function(void *ctx, int val)

    通过内联函数的使用,也算是可以解决“can't jump”这个问题.但同时应当注意,在这其中存在一些限制:

    如果从多个位置调用该函数,则会导致该函数指令在已编译的可执行文件中出现多个副本。有时编译器可能会选择内联函数以进行优化,这也是可能无法将 kprobe 附加到某些内核函数的原因之一。

    从 Linux 4.16 和 LLVM 6.0 开始,取消了要求函数内联的限制,以便 eBPF 程序员可以更自然地编写函数调用。但是,这个称为“BPF to BPF function calls”或“BPF subprograms”的功能目前不受 BCC 框架的支持(当然,如果函数是内联的,则可以继续使用 BCC 的函数。在 eBPF 中,还有另一种将复杂功能分解为更小部分的机制:Tail call

Tail Calls

    “tail calls can call and execute another eBPF program and replace the execution context, similar to how the execve() system call operates for regular processes.”

    也就是说,执行完Tail Calls之后,execution并不会返回给调用方。

    尾部调用可以避免在递归调用函数时一遍又一遍地向堆栈添加帧,这最终可能导致堆栈溢出错误。如果你可以安排你的代码来调用递归函数作为它做的最后一件事,那么与调用函数关联的堆栈帧实际上并没有做任何有用的事情。尾部调用允许在不增加堆栈的情况下调用一系列函数。这在堆栈限制为 512 字节的 eBPF 中特别有用。

    尾部调用是使用 bpf_tail_call() 帮助程序函数进行的,函数原型如下:

long bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)

    三个参数有如下解释:

  • ctx 允许将上下文从调用 eBPF 程序传递给被调用方。
  • prog_array_map 是 BPF_MAP_TYPE_PROG_ARRAY 类型的 eBPF 映射,其中包含一组用于标识 eBPF 程序的文件描述符。
  • index 指示应调用该group中的哪一个eBPF程序。

    要注意的是,正如前面所说:“replace the execution context”。只要这个helper function执行成功,它就不会返回了。当前正在运行的 eBPF 程序在堆栈上将被正在调用的程序替换。但同时,如果这个函数调用失败(比如prog_array_map中没有所指ebpf程序),那么调用方将会继续执行后续代码。

    用户空间代码必须像往常一样将所有 eBPF 程序加载到内核中,并且它还设置了prog_array_map。

以下为BCC下的示例代码:

BPF_PROG_ARRAY(syscall, 300);

int hello(struct bpf_raw_tracepoint_args *ctx) {
    int opcode = ctx->args[1];
    syscall.call(ctx, opcode);
    bpf_trace_printk("Another syscall: %d", opcode);
    return 0;
}

int hello_exec(void *ctx) {
    bpf_trace_printk("Executing a program");
    return 0;
}

int hello_timer(struct bpf_raw_tracepoint_args *ctx) {
    int opcode = ctx->args[1];
    switch (opcode) {
        case 222:
            bpf_trace_printk("Creating a timer");
            break;
        case 226:
            bpf_trace_printk("Deleting a timer");
            break;
        default:
            bpf_trace_printk("Some other timer operation");
            break;
    }
    return 0;
}

int ignore_opcode(void *ctx) {
    return 0;
}

    首先使用宏BPF_PROG_ARRAY()创建syscall[300],在后续代码中,index of array为opcode(operation code),the value代表的是函数指针。

    然后就是即将要绑定到 sys_enter tracepoint 的函数hello()。应当注意:传递给附加到 raw tracepoint (原始追踪点)的eBPF程序的上下文采用此bpf_raw_tracepoint_args结构的形式。

    上下文cxt中的args[1]存储这执行的操作码,然后执行syscall.call(ctx, opcode);(注意:在将源代码传递给编译器之前,BCC 将重写此代码行以调用 bpf_tail_call() 帮助程序函数),如若有与opcode匹配的program则会完成tail call,并且不会执行bpf_trace_printk("Another syscall: %d", opcode);;否则调用失败,hello()会继续执行下去。

    其余函数的作用简单明了。

    完整python代码:

点击查看代码
#!/usr/bin/python3  
from bcc import BPF
import ctypes as ct

program = r"""
BPF_PROG_ARRAY(syscall, 300);

int hello(struct bpf_raw_tracepoint_args *ctx) {
    int opcode = ctx->args[1];
    syscall.call(ctx, opcode);
    bpf_trace_printk("Another syscall: %d", opcode);
    return 0;
}

int hello_exec(void *ctx) {
    bpf_trace_printk("Executing a program");
    return 0;
}

int hello_timer(struct bpf_raw_tracepoint_args *ctx) {
    int opcode = ctx->args[1];
    switch (opcode) {
        case 222:
            bpf_trace_printk("Creating a timer");
            break;
        case 226:
            bpf_trace_printk("Deleting a timer");
            break;
        default:
            bpf_trace_printk("Some other timer operation");
            break;
    }
    return 0;
}

int ignore_opcode(void *ctx) {
    return 0;
}
"""

b = BPF(text=program)
b.attach_raw_tracepoint(tp="sys_enter", fn_name="hello")

ignore_fn = b.load_func("ignore_opcode", BPF.RAW_TRACEPOINT)
exec_fn = b.load_func("hello_exec", BPF.RAW_TRACEPOINT)
timer_fn = b.load_func("hello_timer", BPF.RAW_TRACEPOINT)

prog_array = b.get_table("syscall")
prog_array[ct.c_int(59)] = ct.c_int(exec_fn.fd)
prog_array[ct.c_int(222)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(223)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(224)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(225)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(226)] = ct.c_int(timer_fn.fd)

# Ignore some syscalls that come up a lot
prog_array[ct.c_int(21)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(22)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(25)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(29)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(56)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(57)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(63)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(64)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(66)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(72)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(73)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(79)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(98)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(101)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(115)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(131)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(134)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(135)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(139)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(172)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(233)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(280)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(291)] = ct.c_int(ignore_fn.fd)

b.trace_print()

这些对 b.load_func() 的调用返回每个尾部调用程序的文件描述符。请注意,尾部调用需要具有与其父级相同的程序类型------此例为BPF.RAW_TRACEPOINT。此外,值得指出的是,每个尾部调用程序本身就是一个 eBPF 程序。

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