eBPF 多段程序连接运行(Program Chaining / Tail Call) 是指:一个 eBPF 程序执行完毕后,通过内核指令主动调用另一个 eBPF 程序,形成程序链顺序执行,用来突破单段 eBPF 程序的指令长度限制、实现模块化解耦、复杂逻辑拆分。
核心技术:BPF tail call(bpf_tail_call)
适用场景:网络过滤、流量解析、安全审计、超长逻辑 eBPF 程序。
核心概念
尾调用
eBPF 尾调用(Tail Call)= 程序跳转 + 不返回
就是:当前 eBPF 程序执行到一半,直接跳去执行另一个同类型 eBPF 程序,跳过去就不回来了。
普通函数调用:
你去餐厅吃饭 → 叫服务员 → 服务员回来告诉你 → 你继续吃
(会返回)
尾调用:
你正在吃饭 → 直接瞬移到厨房 → 变成厨师做饭
(再也不回到餐桌了)
内核也是一样:当前程序栈被直接替换成新程序,执行新程序,不返回、不回溯。
核心特性
1. 不返回(最重要)
跳转后原程序立刻停止执行,不会回来继续跑。
2. 同类型才能跳
XDP 只能跳 XDP;
TC 只能跳 TC;
Socket 只能跳 Socket;
不能跨类型跳转!
3. 最多跳 32 次
内核硬限制:一条链最多 32 段程序。
4. 共享同一个上下文
数据包、上下文、内存数据 完全共享。
尾调用的作用
尾调用函数
long bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, __u32 index);
- ctx:程序上下文(如 xdp_md)
- prog_array_map:程序数组映射
- index:要跳转的程序下标(从 0 开始)
关键规则
多段程序链扩展(N 段)
只需两步:
__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 校验不通过
- 缺少指针边界检查
- 循环 / 指令违规
- 程序链不执行
- 未把主程序挂载到网卡
- 跳转下标错误
- 跳了但没反应:没把程序放进 prog_array 跳转表
- 程序加载失败:指针没有做边界检查
- 跳转不生效:程序类型不一致(XDP 跳 TC)
- 超过 32 段:内核直接报错
总结
- eBPF 多段程序靠 bpf_tail_call + PROG_ARRAY 实现链式运行
- 程序必须同类型、最多 32 段
- 用于拆分复杂逻辑、突破指令长度限制
- 提供的 XDP 示例可直接编译运行,验证 stage1 → stage2 跳转
- 尾调用 = 跳转 + 不返回
- 必须用 PROG_ARRAY 程序映射 存放跳转目标

