为什么生成式 BPF loader 对象会在只有子程序或未加载程序把数量推过上限时被拒绝,而检查又如何移到真实的已加载程序数量上?
因为上限是针对对象的程序总数强制执行的,而这个总数把 loader 根本不会去加载的子程序与关闭了 autoload 的程序也算了进去,于是本该能加载的对象也被拒了。修法是把代码生成阶段的记账夹到上限之内,并把真正的拒绝推迟到「程序文件描述符真正写入 loader 栈」的那个时刻;只有那些真正通过了加载遍的程序才会走到那里。
机制
-
固定大小的栈帧。 生成的 loader 本身也是一个 BPF 程序,用来加载对象里的其余部分;它把自己的工作状态放在一个固定大小的栈结构里。这个结构持有 BTF 文件描述符、内部 map 文件描述符,以及一个长度为
MAX_USED_PROGS的程序文件描述符数组。被生成程序里的每个栈地址都是相对这个固定大小的偏移,所以最后一个槽位正好落在栈帧底部,数组之后下一个槽位就是栈指针本身,位于保留区之外。 -
数量为何算错。 对象把程序数暴露为单一数字,这个数字包含作为子程序活在文本段里的静态与全局函数,以及每一个关闭了 autoload 的程序。这些程序都会被加载遍跳过,所以即使真正要加载的程序远低于上限,这个数字也可能顶到上限之上。然而 loader 生成的清理循环却按这个原始数字来定尺寸:每个槽位发一条文件描述符关闭,当数字比上限大一的时候,最后那条关闭就寻址到栈帧末尾之外那个槽位,校验器会拒绝它。
-
三个状态。 主线今天在 loader 生成器里根本没有任何程序数量检查,上限只对 map 存在,因为 map 的数量在 prepare 阶段就定死了,而程序是之后才被过滤的。第一次尝试是在生成阶段按原始数量做前置拒绝,误拒就来自这里:一个只有一个真实程序加三十二个子程序的对象,总数算成三十三,于是生成时就失败,明明它本该能加载。随后那个本周落到下个版本树的跟进补丁,删掉这个前置检查、把清理循环夹到上限之内,并把真正的检查挪到文件描述符写入栈的那一刻,使它只对通过加载遍的程序生效。真正加载超过上限程序的对象仍然以大小错误失败,只是在正确的时机失败了。
验证与排障路径
-
数一数真正被加载的程序。 文本段里的子程序与关闭了 autoload 的程序都会被加载遍跳过,永远不会占用槽位。上限约束的是保持 autoload 打开的入口程序数量。只要这个数不超过上限,对象就应当能加载,无论文本段里有多少子程序。
-
读失败发生的阶段。 在主线树上,生成阶段悄无声息地成功,对象在加载阶段被校验器拒绝栈帧时失败,报出落在栈帧底部的越界访问。在下个版本树上,拒绝发生在生成阶段,loader 会告警程序数量超上限,而做决定的那个数量是真实数量,不是被灌水的那个。同一段告警文字在两个树上含义不同,就是「对象贴近边界」的信号。
-
直接看栈帧。 在生成出的程序里,每条程序文件描述符的写入都指向栈帧内的偏移;一条落在栈指针上的写入或关闭,说明数量超出了栈帧。这个地址就是「拒绝是关于已加载程序数量、而不是关于源文件」最干净的本地证据。
局限
- 那个跟进补丁在下个版本树里,不在主线上。主线仍没有程序数量检查,所以超限对象在主线上是加载阶段失败而非生成阶段失败,由校验器而非 loader 报出来。
- map 的上限在 prepare 阶段就定死,无法像程序上限那样推迟,因为 map 数量决定了生成阶段发出的固定字节偏移的大小。
- 起作用的数量是已加载程序数,不是对象总数,所以检查要放在文件描述符写入那一刻,那里是唯一拿得到被过滤后数量的地方。
References
- 内核
tools/lib/bpf/gen_loader.c— 固定的 loader 栈帧、map 上限,以及主线里未夹值的清理循环。 - 内核
tools/lib/bpf/libbpf.c— 跳过子程序与关闭 autoload 程序的加载遍,以及把原始程序数交给 loader 生成器的那次调用。 - 公开 bpf 邮件列表补丁:只对已加载程序强制程序上限 — 拒绝对照表、越界栈访问,以及夹值清理循环并推迟检查的两处改动。
- 公开 bpf 邮件列表维护者回复,收下该补丁 — 一次 rebase 改写了跟进补丁要修的那个提交,以及它落到下个版本树的过程。
- 公开 bpf 邮件列表作者跟进 — 同日确认已合并。
- 该补丁的自动化评审 — 机器提出的关于 fixes 标签的问题,并不改变最终合并结果。
当日社区讨论
本次运行的观察清单覆盖六个社区:两个聊天工作区共十个频道,两个由存档支撑的聊天工作区共三个频道,一个邮件列表,一个实践者论坛。那三个存档支撑的频道经已批准只读存档按 24 小时与 7 天窗口查询,均未返回任何消息,因此它们是安静而非跳过。两个聊天工作区的十个频道本次未覆盖,因为读取它们需要一次已登录的浏览器会话而本次没有;它们被记为未覆盖,而非安静。
邮件列表上带着本题所涉的 loader 工作。程序上限系列横跨三个时期:2021 年那次把「程序数量过大」不再当作硬错误的放宽、把拒绝挪到生成阶段按原始数量执行的那次提交、以及今天这个更正数量并推迟检查的跟进补丁。维护者在下午收下了跟进补丁——当时一次 rebase 已改写了它要修的那个提交——而一个自动化评审提出了关于 fixes 标签的问题,作者没有回答,这是该系列上唯一未解的线程。
本窗口内第二个更大的邮件列表线程,提出一个针对内核符号表的查询接口,外加一条经它解析无类型内核符号的加载器路径。它身上的评审集中在接口形态而非思路本身,所以它是一个开放的设计问题,而不是一条已定下的边界。
实践者论坛在窗口之内唯一有实质内容的线程,是一篇关于大规模运行 eBPF 的生产经验帖;那里最新的技术讨论已有几天,仍在延续一个基于零拷贝包 socket API 构建的报文构造扫描器。它还有两个未解的问题,也正是工程师会以同样方式撞上的:内核唤醒标志在单个发送环上能换来多少收益,以及忙轮询循环值不值得它付出的那个被占用的核心。
本次运行的存档支撑来源在两个窗口内均未产出消息,因此上面的问题取自被允许的公开 bpf 邮件列表,并经其补丁线程与已发布内核源码核验;实践者论坛提供了本节中的运维主题。
继续阅读
返回索引
eBPF 问答
这里每天收录一个关于 eBPF、Linux 可观测性、性能分析、运行时扩展或安全问题的回答。每篇回答都以公开的一手资料为依据,并在末尾附上匿名化的当日社区讨论摘要。
上一篇 / 上一页
为什么 libbpf 在对象初始加载时会跳过手动策略程序,struct ops 的 map 初始化遍又为什么会把经接口设置的手动加载策略覆盖回去?
因为批量加载遍只加载加载策略为 AUTO 的程序,并故意把 MANUAL 程序保持未加载,好让这个程序之后能被单独加载或卸载。覆盖来自更早的一遍,它跑在对象 prepare 之内:内核 BTF 的 struct ops map 初始化遍在一个回调槽位无法解析时,会无条件地把 DISABLED 写回程序的加载策略,完全不看用户设置的是什么。于是用户经接口设置的 MANUAL 被悄悄改写成 DISABLED,之后的显式加载因为策略已不再是
下一篇 / 下一页
eBPF 问答
这里每天收录一个关于 eBPF、Linux 可观测性、性能分析、运行时扩展或安全问题的回答。每篇回答都以公开的一手资料为依据,并在末尾附上匿名化的当日社区讨论摘要。
- 最后更新
- 2026年10月31日
- 首次发布
- 2026年10月31日
- 贡献者
- Yunwei 123
这个页面有帮助吗?