欢迎光临
我们一直在努力

eBPF 多段程序 连接运行

eBPF 多段程序连接运行(Program Chaining / Tail Call) 是指:一个 eBPF 程序执行完毕后,通过内核指令主动调用另一个 eBPF 程序,形成程序链顺序执行,用来突破单段 eBPF 程序的指令长度限制、实现模块化解耦、复杂逻辑拆分。

核心技术:BPF tail call(bpf_tail_call)

适用场景:网络过滤、流量解析、安全审计、超长逻辑 eBPF 程序。

核心概念

  • 尾调用(Tail Call)内核会替换当前程序上下文,执行新的 eBPF 程序,执行完不返回原程序。
  • 程序链(Program Chain)程序 A → 程序 B → 程序 C → … 最多支持 32 段(内核限制)。
  • 程序数组映射(BPF_MAP_TYPE_PROG_ARRAY)存储所有待链接的 eBPF 程序文件描述符,作为尾调用的 “跳转表”。
  • 尾调用

    eBPF 尾调用(Tail Call)= 程序跳转 + 不返回

    就是:当前 eBPF 程序执行到一半,直接跳去执行另一个同类型 eBPF 程序,跳过去就不回来了。

    普通函数调用:

    你去餐厅吃饭 → 叫服务员 → 服务员回来告诉你 → 你继续吃
    (会返回)

    尾调用:

    你正在吃饭 → 直接瞬移到厨房 → 变成厨师做饭
    (再也不回到餐桌了)

    内核也是一样:当前程序栈被直接替换成新程序,执行新程序,不返回、不回溯。

    核心特性

    1. 不返回(最重要)

    跳转后原程序立刻停止执行,不会回来继续跑。

    2. 同类型才能跳

    XDP 只能跳 XDP;

    TC 只能跳 TC;

    Socket 只能跳 Socket;

    不能跨类型跳转!

    3. 最多跳 32 次

    内核硬限制:一条链最多 32 段程序。

    4. 共享同一个上下文

    数据包、上下文、内存数据 完全共享。

    尾调用的作用

  • 突破 eBPF 单程序指令长度限制(单程序最多 100 万条指令,复杂逻辑不够用)
  • 模块化拆分代码(把大程序拆成多个小程序)
  • 动态选择执行逻辑(根据数据包类型跳不同处理函数)
  • 尾调用函数

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

    • ctx:程序上下文(如 xdp_md)
    • prog_array_map:程序数组映射
    • index:要跳转的程序下标(从 0 开始)

    关键规则

  • 尾调用不返回原程序
  • 最多链式执行 32 段 程序
  • 所有程序必须是同一种类型(XDP 只能链 XDP,TC 只能链 TC)
  • 必须做指针边界检查,否则 eBPF 校验器拒绝加载
  • 跳转表用 BPF_MAP_TYPE_PROG_ARRAY
  • 多段程序链扩展(N 段)

    只需两步:

  • 扩大 max_entries

    __uint(max_entries, 4); // 4 段程序

  • 新增程序,并写入跳转表

    bpf_map_update_elem(map_fd, &(int){2}, &prog_fd3, BPF_ANY);
    bpf_map_update_elem(map_fd, &(int){3}, &prog_fd4, BPF_ANY);

  • 在程序中继续用 bpf_tail_call 跳转即可。

    BPF_MAP_TYPE_PROG_ARRAY

    BPF_MAP_TYPE_PROG_ARRAY 是 eBPF 尾调用(Tail Call)的核心载体;

    BPF_MAP_TYPE_PROG_ARRAY = 存放 eBPF 程序的 “跳转表”

    • 它是一种特殊的 eBPF map
    • 专门用来存储多个 eBPF 程序的文件描述符(FD)
    • 作用:让 bpf_tail_call 能根据下标找到并跳转到目标程序

    没有它,尾调用完全无法实现。

    把它当成一个电梯按钮面板:

    • 按钮 0 → 1 楼程序
    • 按钮 1 → 2 楼程序
    • 按钮 2 → 3 楼程序

    bpf_tail_call(ctx, map, 1)= 按 1 楼按钮,直接跳过去,不回来

    核心特性

  • 只能存 eBPF 程序 FD,不能存普通数据,只能存已加载的 eBPF 程序文件描述符。

  • key 是整数下标(0、1、2…),像数组一样,用数字索引查找程序。

  • value 是 int 类型(程序 FD),内核用这个 FD 找到要跳转的程序。

  • 所有程序必须是同一种类型,XDP 只能存 XDP,TC 只能存 TC,不能混放。

  • 内核限制最大 32 个条目,对应尾调用最多 32 段程序。

  • 标准定义格式(固定写法)

    这是你在 eBPF C 代码里必须写的格式,直接复制就能用:

    // 程序数组映射(尾调用跳转表)
    struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY); // 类型固定
    __uint(max_entries, 4); // 最多存 4 个程序(可改)
    __uint(key_size, sizeof(__u32)); // key 固定是 32 位整数
    __uint(value_size, sizeof(int)); // value 固定是 int(程序FD)
    } prog_array SEC(".maps"); // 挂载到 maps 段

    eBPF 程序侧:定义 map

    struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 2);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, sizeof(int));
    } prog_array SEC(".maps");

    SEC("xdp")
    int stage1(struct xdp_md *ctx)
    {
    // 满足条件 → 跳转到下标 1 的程序
    bpf_tail_call(ctx, &prog_array, 1);
    return XDP_PASS;
    }

    SEC("xdp")
    int stage2(struct xdp_md *ctx)
    {
    return XDP_DROP;
    }

    用户态侧:把程序 “放进” map

    这一步必须做,否则跳转表是空的,跳不过去!

    // 获取两个程序的 FD
    int fd1 = bpf_program__fd(prog1);
    int fd2 = bpf_program__fd(prog2);

    // 写入 map:下标 0 → 程序1,下标1 → 程序2
    bpf_map_update_elem(map_fd, &(int){0}, &fd1, BPF_ANY);
    bpf_map_update_elem(map_fd, &(int){1}, &fd2, BPF_ANY);

    内核执行跳转

    运行 stage1 → 调用 bpf_tail_call → 查 prog_array[1] → 找到 stage2 → 执行 stage2

    极简示例

    // 跳转表(存放多个 eBPF 程序)
    struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 2);
    } progs SEC(".maps");

    // 程序 A:第一段
    SEC("xdp")
    int progA(struct xdp_md *ctx)
    {
    // 满足条件 → 跳转到程序 B(下标 1)
    if (条件)
    bpf_tail_call(ctx, &progs, 1);

    // 不满足 → 直接放行
    return XDP_PASS;
    }

    // 程序 B:第二段(被跳转)
    SEC("xdp")
    int progB(struct xdp_md *ctx)
    {
    // 跳转过来后直接执行这里
    return XDP_DROP;
    }

    执行流程:

    progA 运行 → 满足条件 → 跳转到 progB → 执行 progB → 结束
    (不回到 progA)

    尾调用 vs 普通函数调用

    特性普通函数调用eBPF 尾调用
    是否返回
    栈是否保留 保留 替换 / 销毁
    指令长度 受限制 不受限制
    程序类型 任意 必须相同

    常见问题

    • 尾调用失败
      • 下标越界
      • 程序类型不匹配
      • 未加载到跳转表
    • eBPF 校验不通过
      • 缺少指针边界检查
      • 循环 / 指令违规
    • 程序链不执行
      • 未把主程序挂载到网卡
      • 跳转下标错误
    • 跳了但没反应:没把程序放进 prog_array 跳转表
    • 程序加载失败:指针没有做边界检查
    • 跳转不生效:程序类型不一致(XDP 跳 TC)
    • 超过 32 段:内核直接报错

    总结

    • eBPF 多段程序靠 bpf_tail_call + PROG_ARRAY 实现链式运行
    • 程序必须同类型、最多 32 段
    • 用于拆分复杂逻辑、突破指令长度限制
    • 提供的 XDP 示例可直接编译运行,验证 stage1 → stage2 跳转
    • 尾调用 = 跳转 + 不返回
    • 必须用 PROG_ARRAY 程序映射 存放跳转目标
    赞(0)
    未经允许不得转载:171主机测评 » eBPF 多段程序 连接运行
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址