Overview Link to heading

BPF 在网络、追踪、安全等 Linux 内核领域的可扩展性及其在用户空间 BPF 虚拟机中的多场景应用,导致外界对 BPF 的本质存在诸多误解。本问答短文旨在澄清这些误解,并勾勒出 BPF 长远发展的方向。

Q: BPF 是不是和 x64、arm64 类似的通用指令集? Link to heading

A:不是

Q:BPF 是一种通用虚拟机吗? Link to heading

A:不是。BPF 是一种通用的指令集,支持 C 语言的调用约定。

Q:为什么选择 C 调用约定? Link to heading

A:由于 BPF 程序是为运行在用 C 语言编写的 Linux 内核中而设计的,因此 BPF 定义了一套与最常用的两种架构 x64 和 arm64 兼容的指令集(同时考虑了其他架构的重要特性),并定义了与这些架构上 Linux 内核 C 语言调用约定兼容的调用约定。

Q:未来是否支持多个返回值? Link to heading

A:不会,BPF 仅允许寄存器 R0 用作返回值。

Q:未来是否支持超过 5 个函数参数? Link to heading

A:不会,BPF 调用约定仅允许使用 R1-R5 寄存器作为参数。BPF 并非独立的指令集。(不同于 x64 ISA,它支持 msft、cdecl 等调用约定)

Q:BPF 程序能否访问指令指针或返回地址? Link to heading

A:不能。

Q:BPF 程序能否访问栈指针? Link to heading

A:不能。只有帧指针(寄存器 R10 )是可用的。从编译器的角度来看,必须要有栈指针。例如,LLVM 在其 BPF 后端中将寄存器 R11 定义为栈指针,但它确保生成的代码永远不会使用它。

Q:C 调用约定是否会减少潜在的应用场景? Link to heading

A:会。BPF 的设计要求通过内核辅助函数和内核对象(如 BPF 映射)来增加主要功能,并实现它们之间无缝的互操作性。它允许内核调用 BPF 程序,同时程序也能调用内核辅助函数,且没有任何额外开销,因为所有这些代码都是原生 C 代码。对于经过 JIT 编译的 BPF 程序来说尤其如此,它们与原生内核 C 代码无法区分。

Q:这是否意味着对 BPF 代码的“创新性”扩展是不被允许的? Link to heading

A:不全是,至少目前而言,直到 BPF 核心支持 bpf-to-bpf 调用、间接调用、循环、全局变量、跳转表、只读段以及其他所有 C 代码能产生的常规结构。

Q:循环能否安全地实现? Link to heading

A:还不清楚。BPF 开发者正试图寻找一种支持有界循环的方法。

Q:验证器有什么限制? Link to heading

A:用户空间唯一已知的限制是 BPF_MAXINSNS(4096),即无特权 bpf 程序所能包含的最大指令数。验证器内部存在多种限制,例如程序分析期间可探索的最大指令数。目前该限制设置为 100 万,这意味着最大程序可由 100 万条 NOP 指令构成。后续分支数量、嵌套 bpf 互调次数、每条指令的验证器状态数量以及程序使用的 map 数量均存在上限。这些限制都可能被足够复杂的程序触发。此外,还存在非数值限制可能导致程序被拒绝。验证器曾仅支持指针+常量表达式,现可识别指针+有界寄存器。bpf_lookup_map_elem 函数要求 ‘key’ 必须是指向栈的指针,现 ‘key’ 可为指向 map 值的指针。验证器正稳步提升智能化水平,部分限制已被移除。唯一能确定程序能否被验证器接受的方法是尝试加载。bpf 开发流程保证,未来内核版本将接受所有先前版本已接受的 bpf 程序。