Overview Link to heading
BPF 疑惑解答系列第二篇,本篇内容更偏理论。
LD_ABS 和 LD_IND 指令与 C 代码的比较 Link to heading
Q:为什么 BPF 中存在 LD_ABS 和 LD_IND 指令,而 C 语言无法直接表达它们,只能使用内置的 intrinsics 呢? Link to heading
A:这是为了兼容经典 BPF 而存在的特性。现代 BPF 网络代码在没有它们的情况下性能更佳。参见"直接数据包访问"。
BPF 指令与原生 CPU 之间并非一一对应 Link to heading
Q:似乎并非所有 BPF 指令都能与原生 CPU 指令一一对应。比如,为什么 BPF_JNE 和其他比较及跳转指令不像 CPU 指令那样呢? Link to heading
A:这势在必行,以避免将那些在 CPU 架构间难以实现通用化和高效化的特性标志引入到指令集架构(ISA)中。
Q:为什么 BPF_DIV 指令不能映射到 x64 div(除法指令)? Link to heading
A:如果我们选择与 x64 建立一对一的关系,那将使在 arm64 和其他架构上的支持变得更加复杂。此外,它还需要进行除零运行时检查。
Q:BPF 为什么有隐式的序言和尾声? Link to heading
A:因为像 sparc 这样的架构有寄存器窗口,而且一般来说不同架构之间都有细微的差异,所以直接将返回地址存入栈的方法行不通。另一个原因是 BPF 必须确保不会发生除零错误(以及 LD_ABS 指令的旧异常路径)。这些指令需要调用尾声部分,并隐式返回。
Q:为什么 BPF_JLT 和 BPF_JLE 指令最初没有被引入? Link to heading
A:因为经典 BPF 没有这些指令,而且 BPF 开发者觉得编译器的工作绕过方式是可以接受的。结果发现,由于缺少这些比较指令,程序的性能会下降,所以这些指令最终被添加了进来。这两条指令是未来哪些新的 BPF 指令可以被接受并添加的完美范例。实际上,这两条指令在原生 CPU 上已经有了等效的指令。那些没有与硬件指令一一对应的全新指令将不会被接受。
BPF 32 位子寄存器要求 Link to heading
Q:BPF 的 32 位子寄存器需要将 BPF 寄存器的上 32 位清零,这使得 BPF 在 32 位 CPU 架构和 32 位硬件加速器上效率不高。未来 BPF 能否增加真正的 32 位寄存器? Link to heading
A:不会。
但针对 BPF 寄存器清除高 32 位的某些优化措施已经可用,并能用来提升 32 位架构下 JIT 编译的 BPF 程序性能。
从 7 版本开始,只要编译程序时传入选项 “-mattr=+alu32
",LLVM 就能生成对 32 位子寄存器操作的指令。此外,验证器现在能标记需要清除目标寄存器高位的指令,并插入显式的零扩展(zext)指令(mov32 的变种)。这意味着对于没有硬件 zext 支持的架构,JIT 后端无需清除 alu32 指令或窄加载写入的子寄存器高 8 位。后端只需支持生成该 mov32 变种的代码,并重写 bpf_jit_needs_zext()
使其返回 “true”(以在验证器中启用 zext 插入)。
需要注意的是,JIT 后端可能存在部分硬件 zext 支持。若此时验证器启用了 zext 插入,可能会导致插入不必要的 zext 指令。这种情况可通过在 JIT 后端内创建一个简单的窥孔优化来处理:若某条指令有硬件 zext 支持,且下一条指令是显式的 zext,则在代码生成时可以跳过后者。