bpftool prog dump jited id 42 输出了一段
x86-64
汇编。rax、rdi、rsi
的名字你全认识,但有一个关键问题直接决定了你是否理解 JIT
的效率边界:为什么 BPF 的 BPF_ALU32 加法在
x86-64 上往往只需一条 addl,而在 ARM64
上却可能需要额外的 uxtw 来清零高 32 位?为什么
JIT 不能总是生成最优本地码?
本文拆解 BPF JIT 编译器的完整翻译管线:从
bpf_jit_compile() 的架构调度入口,到 x86-64 和
ARM64 两个后端的寄存器映射、ALU
翻译策略、分支和调用实现、尾调用机制,再到 JIT 镜像的
kallsyms 暴露和 bpf_jit_limit
的资源限制。读完后,你理解 JIT
编译不仅是一个”字节码→本地码”的简单转换,而是一个受限于 BPF
ISA 表达力的有损翻译过程。
一、JIT 解决的问题和入口
1.1 Interpreter vs JIT 的性能差异
当 BPF 程序被 verifier 接受但 JIT 未启用时,内核使用 BPF 解释器(interpreter)来执行程序:
/* Linux 6.6: kernel/bpf/core.c — BPF 解释器的核心循环 */
static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
{
u64 stack[MAX_BPF_STACK / sizeof(u64)];
...
select_insn:
goto *jumptable[insn->code];
...
}解释器的核心是一个间接跳转表——每条 BPF 指令通过
jumptable 分发到对应的
handler。每条指令至少涉及:两次内存加载(取指令 + 解引用
jumptable),一次间接跳转,以及实际的计算逻辑。在现代超标量处理器上,间接跳转会破坏分支预测,导致流水线停顿。
典型性能差异(定性描述,具体倍率依 CPU 与程序形态而定,需在本机 benchmark 验证):
| 场景 | Interpreter 特点 | JIT 特点 |
|---|---|---|
| 简单 ALU(BPF_MOV) | 每条指令经跳转表分发,间接跳转多 | 通常可一对一映射为单条本地 mov |
| BPF_LD_IMM64 | 双宽指令 + 分发开销 | x86-64 上常见为 movabsq 等少量指令 |
| 密集算术循环 | 每步都有分发与取指开销 | 本地码可合并、寄存器分配更灵活 |
| helper 调用 | 包装层 + 分发 | 仍有序言与寄存器保存,但无解释器循环 |
平均而言,JIT 相对解释器通常快数倍到一个数量级;对于网络数据面(XDP、TC),生产环境普遍启用 JIT——interpreter 难以支撑高吞吐线速处理。
1.2 JIT 的架构调度入口
bpf_jit_compile() 是 JIT
编译的架构无关入口:
/* Linux 6.6: kernel/bpf/core.c */
struct bpf_prog *bpf_jit_compile(struct bpf_prog *prog)
{
/* 1. 检查是否启用 JIT */
if (!bpf_jit_enable)
return prog;
/* 2. 检查是否有对应架构的 JIT 编译器 */
if (!bpf_jit_compiler_ops[prog->jit_arch])
return prog;
/* 3. 调用架构特定的 JIT 编译函数 */
prog = bpf_int_jit_compile(prog);
/* 4. 如果 JIT 失败,保留为 interpreter 模式 */
return prog;
}bpf_int_jit_compile()
是架构特定的函数指针。对于 x86-64,这个函数定义在
arch/x86/net/bpf_jit_comp.c;对于 ARM64,定义在
arch/arm64/net/bpf_jit_comp.c。
/* Linux 6.6: arch/x86/net/bpf_jit_comp.c — x86-64 入口 */
struct bpf_prog *bpf_int_jit_compile(struct bpf_prog *prog)
{
struct bpf_binary_header *header = NULL;
struct bpf_prog *tmp, *orig_prog = prog;
int proglen, pass;
u8 *image = NULL;
/* 第 1 pass:计算镜像大小 */
/* 第 2 pass:生成实际机器码 */
...
}JIT 使用两轮编译(two-pass)策略:第一轮计算需要的镜像大小(因为 x86-64 的每条 BPF 指令可能生成 1-20 字节不等的本地码),第二轮填充实际的机器码字节。
二、x86-64 JIT 后端
2.1 寄存器映射
x86-64 JIT 的寄存器映射遵循 System V AMD64 ABI 的调用约定:
| BPF 寄存器 | x86-64 寄存器 | 选择理由 |
|---|---|---|
| R0 (返回值) | rax |
x86-64 ABI 返回值寄存器 |
| R1 (arg0) | rdi |
x86-64 ABI 第 1 参数 |
| R2 (arg1) | rsi |
ABI 第 2 参数 |
| R3 (arg2) | rdx |
ABI 第 3 参数 |
| R4 (arg3) | rcx |
ABI 第 4 参数 |
| R5 (arg4) | r8 |
ABI 第 5 参数 |
| R6 (callee-saved) | rbx |
callee-saved in ABI |
| R7 (callee-saved) | r13 |
callee-saved (low regs avoid saving) |
| R8 (callee-saved) | r14 |
callee-saved |
| R9 (callee-saved) | r15 |
callee-saved |
| R10 (frame) | rbp |
x86-64 frame pointer |
寄存器映射的实现见
arch/x86/net/bpf_jit_comp.c 中的
bpf2reg[] 数组(将 BPF 寄存器编号映射到 x86
寄存器编码)。上表即为 R0–R10 到 x86-64
寄存器的对应关系,无需再引用源码中的编码细节。
x86-64 JIT 的映射有以下几个工程考量:
R1-R5 直接映射到 ABI 参数寄存器:这使得从 JIT 代码调用 helper 函数时不需要寄存器的移动——BPF R1 就是
rdi,而rdi就是 C ABI 的 arg0。调用 helper 直接就是call helper_func。R6-R9 使用 callee-saved 寄存器:
rbx、r13、r14、r15在 x86-64 ABI 中是 callee-saved 的。这意味着 JIT 生成的代码在调用 helper 时不需要显式保存这些寄存器——C 调用约定已经保证 helper 不会修改它们。如果选择了 caller-saved 寄存器(如r10、r11),每次 helper 调用都需要额外的push/pop。R10 是
rbp:BPF 的帧指针映射到 x86-64 的基址指针。这允许使用 x86-64 的自然[rbp - offset]寻址模式。
2.2 ALU 指令翻译
ALU64(64-bit 操作)——one-to-one 翻译:
| BPF 指令 | x86-64 指令 | 编码示例(BPF R0 += BPF R1) |
|---|---|---|
BPF_ADD |
addq %r1, %r0 |
48 01 c8 |
BPF_SUB |
subq %r1, %r0 |
48 29 c8 |
BPF_MUL |
imulq %r1, %r0 |
48 0f af c1 |
BPF_DIV |
divq %r1 (需先
xor %edx,%edx) |
48 f7 f1 |
BPF_OR |
orq %r1, %r0 |
48 09 c8 |
BPF_AND |
andq %r1, %r0 |
48 21 c8 |
BPF_LSH |
shlq %cl, %r0 (移位量必须先存到
%cl) |
48 d3 e0 |
BPF_RSH |
shrq %cl, %r0 |
48 d3 e8 |
BPF_ARSH |
sarq %cl, %r0 |
48 d3 f8 |
BPF_NEG |
negq %r0 |
48 f7 d8 |
BPF_XOR |
xorq %r1, %r0 |
48 31 c8 |
BPF_MOV |
movq %r1, %r0 |
48 89 c8 |
立即数变体(BPF_K 而非
BPF_X)使用 x86-64
的立即数指令编码(addq $imm, %r0)。
ALU32(32-bit 操作)——利用 x86-64 零扩展特性:
x86-64 的一个关键特性是:任何 32-bit
寄存器操作会自动将高 32-bit 清零。例如
xor %eax, %eax 清零了整个 64-bit
rax。JIT 利用这一特性实现 BPF_ALU32:
/* BPF_ALU32: dst = (u32)dst OP (u32)src */
/* x86-64 JIT 翻译(以 ADD 为例): */
/* BPF: r0 += r1 (32-bit) */
/* x86: addl %r1d, %r0d // 32-bit 操作自动清空 r0 的高 32-bit */这意味着 BPF_ALU32 的操作序列比 ALU64
更紧凑——不需要显式的 mov %eax, %eax 来清零高
32-bit。
BPF_LD_IMM64——双宽指令的特殊处理:
/* BPF_LD_IMM64 dst_reg, imm64 */
/* x86-64 翻译:movabsq $imm64, %dst_reg */
/* 占用 10 字节(2 字节 opcode + 8 字节立即数)*/movabsq 是 x86-64 中唯一能加载 64-bit
立即数的指令,它使用完整的 8 字节立即数编码。
BPF_END 端序转换:
/* BPF_TO_LE (在小端主机上就是 NOP) */
/* BPF_TO_BE on x86: bswap + 右移 */
/* 例如 BE16: rol $8, %reg ; movzwl %reg, %reg */2.3 内存访问
加载:
/* BPF_LDX_DW r0, [r1 + off] */
/* x86-64: movq off(%r1), %r0 */
/* 编码:48 8b 41 off (off 在 [-128, 127] 时)*/x86-64 的寻址模式天然支持
base + displacement,与 BPF 的
[src_reg + off] 模型完美匹配。
存储:
/* BPF_STX_W [r10 + off], r0 */
/* x86-64: movl %r0d, off(%rbp) */2.4 函数调用
Helper
函数调用(BPF_CALL):
/* BPF: call helper#N */
/* x86-64 JIT 翻译序列: */
/* 1. 保存 caller-saved 寄存器(如果需要) */
/* 2. movabsq $__bpf_call_base + N*8, %rax */
/* 3. call *(%rax) */
/* 4. 恢复 caller-saved 寄存器(如果需要) */helper 调用通过 __bpf_call_base
表间接跳转。__bpf_call_base
是一个函数指针数组,索引为 helper ID:
/* Linux 6.6: kernel/bpf/core.c */
const struct bpf_func_proto *bpf_get_trace_printk_proto(void);
...
void * __bpf_call_base = __bpf_call_base_arr;
/* 运行时,地址计算为 __bpf_call_base_arr[helper_id * 8] */程序内调用(BPF_PSEUDO_CALL):
程序内调用直接使用 x86-64 的 call
指令跳转到目标子程序的 JIT 镜像内地址。JIT
在编译时计算相对偏移:
/* x86-64: call rel32_offset */
/* rel32 = target_addr - (current_addr + 5) */这与 x86-64 上正常的 C 函数调用完全一致。
2.5 尾调用(Tail Call)
尾调用是 BPF 中特殊的”长跳转”机制——允许一个 BPF
程序直接跳转到另一个 BPF 程序入口(不返回)。BPF 程序侧通过
helper bpf_tail_call() 触发;x86-64 JIT 内部由
emit_bpf_tail_call()(或等价路径)生成本地码。
/* Linux 6.6: arch/x86/net/bpf_jit_comp.c — tail call JIT 序列(简化) */
/* 尾调用的 x86-64 序列: */
/* 1. 检查 prog_array map 中的目标程序是否存在 */
/* 2. 如果目标为 NULL:继续执行当前程序 */
/* 3. 如果目标存在:准备新的栈帧,jmp 到目标程序的 JIT 入口 */
/* 实际生成的代码(简化) */
/* lea -STACK_SIZE(%rbp), %rsp ; 重置栈指针到目标程序的帧 */
/* jmp *target_entry ; 直接跳转到目标程序 */尾调用的性能开销:每次尾调用约 10–15 条本地指令(map 查找 + 栈重建 + 跳转),在 x86-64 上约 5–10 ns。比完整函数调用(需要 push/pop 和返回)快约 2–3 倍,比重新从 interpreter 入口进入快约 10 倍。
2.6 JIT
优化:bpf_jit_optimize
x86-64 JIT 在两轮编译之间执行轻量级优化:
/* Linux 6.6: arch/x86/net/bpf_jit_comp.c */
static void bpf_jit_optimize(struct bpf_prog *prog)
{
/* 1. 消除 NOP 指令(BPF_NOP/jmp 0) */
/* 2. 转换冗余的 MOV (mov %rA, %rB; mov %rB, %rA → 单条 mov) */
/* 3. 将条件跳转的 NEG+JMP 合并为单条 JMP(翻转条件) */
/* 4. 消除死代码(在 verifier 已经保证了可达性的前提下有限) */
}这些优化在编译时对 BPF 指令流进行,然后在第二 pass 生成优化后的机器码。与 LLVM/GCC 的优化流水线相比非常有限,主要是因为 BPF 经过 clang -O2 编译后已经优化过,JIT 只需要做字节码模式级别的转换。
三、ARM64 JIT 后端
3.1 寄存器映射
ARM64 JIT 的寄存器映射与 x86-64 有根本不同:
| BPF 寄存器 | ARM64 寄存器 | 选择理由 |
|---|---|---|
| R0 | x7 |
ARM64: x0-x7 是参数/返回值寄存器 |
| R1 | x0 |
映射到 ARM64 ABI arg0(与 x86 的 rdi 的角色相同,但寄存器号不同) |
| R2 | x1 |
arg1 |
| R3 | x2 |
arg2 |
| R4 | x3 |
arg3 |
| R5 | x4 |
arg4 |
| R6 | x19 |
callee-saved (x19-x28) |
| R7 | x20 |
callee-saved |
| R8 | x21 |
callee-saved |
| R9 | x22 |
callee-saved |
| R10 | x25 |
用作帧指针(ARM64 上 x29 是硬件 FP,但被 JIT 保留) |
| TCC | x26 |
尾调用计数器(BPF_MAX_TAIL_CALL_CNT 检查) |
ARM64 的映射刻意将 BPF R1-R5 对齐到 ARM ABI 的 x0-x4——这使得 helper 调用的参数传递与 BPF 调用约定自然重合,无需寄存器移动。
3.2 ALU64 翻译
ARM64 上的 64-bit ALU 指令大多是 one-to-one 的:
| BPF 指令 | ARM64 指令 |
|---|---|
BPF_ADD |
add xD, xA, xB |
BPF_SUB |
sub xD, xA, xB |
BPF_MUL |
mul xD, xA, xB |
BPF_DIV |
udiv xD, xA, xB |
BPF_OR |
orr xD, xA, xB |
BPF_AND |
and xD, xA, xB |
BPF_LSH |
lsl xD, xA, xB |
BPF_RSH |
lsr xD, xA, xB |
BPF_ARSH |
asr xD, xA, xB |
BPF_NEG |
neg xD, xA |
但有一个关键例外:BPF_MUL (64-bit)。
BPF: r0 = r0 * r1 (完整的 128-bit 结果截断到低 64-bit)
ARM64: mul x7, x7, x0 // ARM64 的 mul 指令也产生 64-bit 截断结果
// 这是兼容的——BPF 和 ARM64 都丢弃溢出高位
// 但 x86-64 的 imulq 是一条指令,ARM64 的 mul 也是一条——两者的 64-bit 乘法性能相同
3.3 ALU32 翻译:ARM64 的额外负担
这是 ARM64 JIT 和 x86-64 JIT 最显著的差异。ARM64 没有”32-bit 操作自动清零高 32-bit”的硬件特性。BPF_ALU32 的语义要求在操作后将目标寄存器的高 32-bit 清零,这在 ARM64 上需要显式指令:
BPF: r0 += r1 (32-bit)
x86-64: addl %r1d, %r0d ; 1 条指令,自动零扩展
ARM64: add w7, w7, w0 ; 1 条指令(使用 w 寄存器自动截断)
; 但高 32-bit 未清零!
; 需要显式 uxtw x7, w7 ; 或者 mov w7, w7
在实践中,JIT 编译器会追踪是否需要清零高
32-bit。如果后续指令也会覆盖完整的 64-bit 寄存器,则可以省略
uxtw:
/* ARM64 JIT 的优化逻辑:推迟 uxtw 到真正需要 64-bit 值的时候 */
/* 如果多条 BPF_ALU32 连续执行,只在最后一条或需要 64-bit 使用前插入 uxtw */这导致 ARM64 上密集的 32-bit 算术比等效的 64-bit 算术有额外开销。
3.4 原子操作
ARM64 支持通过 LSE(Large System Extensions)指令集实现高效的原子操作:
| BPF 原子操作 | ARM64 指令(LSE) | ARM64 指令(无 LSE) |
|---|---|---|
BPF_ATOMIC_ADD |
stadd |
ldaxr; add; stlxr (LL/SC 循环) |
BPF_ATOMIC_FETCH_ADD |
ldaddal |
LL/SC loop with old value |
BPF_ATOMIC_XCHG |
swpal |
LL/SC loop |
BPF_ATOMIC_CMPXCHG |
casal |
LL/SC loop |
如果硬件支持 LSE(ARMv8.1+),原子操作为 one-to-one 翻译;否则退化为 load-linked/store-conditional 循环。
3.5 Tail Call 在 ARM64 上的实现
ARM64 的 tail call 实现与 x86-64 在语义上相同,但使用了不同的寄存器:
/* ARM64 JIT 中的 tail call 实现(简化) */
/* 1. 将 TCC (x26) 递增,检查是否达到 BPF_MAX_TAIL_CALL_CNT */
/* 2. 查询 prog_array map(使用 R1/R2 作为参数) */
/* 3. 如果找到目标:构建新帧,br 到目标入口 */
/* 4. 否则继续当前程序 */ARM64 使用专用寄存器 x26
作为尾调用计数器(TCC),避免每次尾调用都需要从内存重新加载计数器。
四、JIT 资源限制与调试集成
4.1 bpf_jit_limit
JIT
编译的镜像需要分配可执行内核内存。为防止单个用户消耗过多内存,内核设置了
bpf_jit_limit:
# 查看和设置 JIT 内存限制
cat /proc/sys/net/core/bpf_jit_limit
# 默认值:PAGE_SIZE * 4000 ≈ 16 MB(在 4K 页大小系统上)
# 或者:min(PAGE_SIZE * 4000, totalram_pages / 4)
# 设置自定义限制
echo 33554432 > /proc/sys/net/core/bpf_jit_limit # 32 MB当 JIT 内存耗尽时,内核会返回 -ENOMEM 并跳过
JIT 编译,程序退化为 interpreter 模式执行。
4.2
bpf_jit_kallsyms
JIT 编译的函数镜像被暴露在 /proc/kallsyms
中,以便调试和性能分析工具关联 JIT 生成的代码:
# 查看 JIT'd BPF 程序的内核地址
grep bpf_prog /proc/kallsyms | head
# 输出示例:
# ffffffff81001234 t bpf_prog_42_xdp_filter [bpf]
# ffffffff81001500 t bpf_prog_43_tc_ingress [bpf]每个 JIT 镜像以
bpf_prog_<ID>_<name>
的格式注册。这使得 perf record 和
perf report 能够按名称显示 BPF 程序的 CPU
使用率:
# 采样 5 秒后查看调用栈(需 root 且 kallsyms 可见)
sudo perf record -g -- sleep 5
sudo perf report --sort comm,dso,symbol | grep bpf_prog
# 输出示例:
# 15.23% bpf_prog_42_xdp_filter [bpf]
# 8.45% bpf_prog_43_tc_ingress [bpf]4.3 JIT Hardening
bpf_jit_harden 控制 JIT 代码的安全加固:
# 0 = 不加固(默认)
# 1 = 对非特权用户启用加固
# 2 = 对所有用户启用加固(最佳安全性,但有性能代价)
echo 2 > /proc/sys/net/core/bpf_jit_harden加固机制包括:
- 常数盲化(Constant blinding):将 BPF 程序中的 64-bit 立即数通过 XOR 操作盲化,防止攻击者利用 JIT 镜像中的已知常数构建 ROP/JOP 链。
- Retpoline:使用 retpoline
模式生成间接跳转(
call *(%rax)替换为lfence; jmp *(%rax)),防止 Spectre v2 攻击。
常数盲化会引入额外的 XOR 等指令,典型开销为数个百分点(依程序形态而定,需在本机 benchmark 验证)。
五、Interpreter vs JIT 对比
| 维度 | Interpreter | JIT |
|---|---|---|
| 加载延迟 | 极小(直接开始执行) | ~10-100us 编译开销 |
| 执行速度 | 基准值 | 通常快数倍到一个数量级(依程序与 CPU 而定) |
| 内存占用 | 无额外内存 | 每条指令 1–20 字节镜像 |
| 调试支持 | 容易(单步执行) | 困难(需要反汇编 JIT 镜像) |
| 安全加固 | N/A(interpreted 无 ROP 风险) | 常数盲化、retpoline |
| 适用场景 | 一次性追踪脚本、测试 | 高吞吐数据面(XDP、TC) |
JIT 通过 bpf_jit_enable sysctl
控制是否启用。生产环境中应始终设置为 1(启用)。
六、Mermaid:JIT 编译与执行流程
sequenceDiagram
participant USER as 用户态 (libbpf/bpftool)
participant SYS as bpf() syscall
participant VERIF as verifier (kernel/bpf/verifier.c)
participant CORE as bpf_jit_compile()<br/>(kernel/bpf/core.c)
participant X86 as x86-64 JIT<br/>(arch/x86/net/bpf_jit_comp.c)
participant ARM64 as ARM64 JIT<br/>(arch/arm64/net/bpf_jit_comp.c)
participant MEM as 内核可执行内存
participant HOOK as BPF hook (XDP/TC/kprobe)
participant KP as kallsyms
USER->>SYS: bpf(BPF_PROG_LOAD, insns, size)
SYS->>VERIF: bpf_check(prog) → 逐条验证
VERIF-->>SYS: 验证通过 ✓
SYS->>CORE: bpf_jit_compile(prog)
CORE->>CORE: 检查 bpf_jit_enable<br/>检查 bpf_jit_limit
alt x86-64 系统
CORE->>X86: bpf_int_jit_compile(prog)
X86->>X86: Pass 1: 计算镜像大小<br/>寄存器分配 + 指令选择
X86->>X86: bpf_jit_optimize(prog)<br/>消除 NOP、合并 MOV
X86->>MEM: Pass 2: emit 机器码<br/>分配 module_alloc() 内存
else ARM64 系统
CORE->>ARM64: bpf_int_jit_compile(prog)
ARM64->>ARM64: Pass 1: 计算镜像大小<br/>寄存器分配 (x0-x4, x19-x25)
ARM64->>ARM64: 插入 uxtw 指令<br/>(ALU32 高 32-bit 清零)
ARM64->>MEM: Pass 2: emit ARM64 码<br/>分配可执行内存
end
X86-->>CORE: 返回 bpf_binary_header
ARM64-->>CORE: 返回 bpf_binary_header
CORE->>KP: 注册 JIT 镜像<br/>bpf_jit_kallsyms_add()
CORE-->>SYS: 返回编译后的 prog
SYS-->>USER: 返回程序 FD
Note over HOOK: 运行时:attach 到 hook
USER->>HOOK: attach (XDP/netlink/perf_event)
HOOK->>MEM: call bpf_prog_42_xdp_filter
Note over MEM: 本地指令直接执行<br/>在 CPU 上全速运行
七、为什么 JIT 不能总是生成最优码
从上面的分析可以总结出 JIT 翻译质量的几个根本限制:
BPF ISA 的表达力上限:BPF 没有浮点指令、没有 SIMD/向量指令、没有条件执行(ARM64 的
csel等)。JIT 无法凭空生成这些优化。寄存器压力:BPF 只有 11 个寄存器,比 x86-64 的 16 个(或 ARM64 的 31 个)少得多。JIT 必须精确映射,无法利用多余的寄存器做 loop unrolling 或指令调度。
调用约定耦合:BPF 的 R1-R5 必须映射到 ABI 参数寄存器,这固化了寄存器分配。编译器优化中常见的寄存器重命名、循环流水等优化不可行。
无全局优化:JIT 只在单条 BPF 指令级别做模式匹配(
BPF_ADD → addq),不做跨指令优化。例如连续的BPF_MOV + BPF_ADD不能自动合并为lea(x86-64 的地址计算指令)。安全约束:常数盲化和 retpoline 增加指令数。在生产环境中启用
bpf_jit_harden=2后,JIT 代码的性能通常会有可感知的下降(具体幅度需 benchmark)。
八、小结
BPF JIT 编译器在架构设计上是精巧的折中:它在两轮编译中完成从 BPF 字节码到本地指令的翻译,利用与目标架构 ABI 的对齐来最小化函数调用开销,通过寄存器映射和指令模式匹配实现快速的 one-to-one 或 few-to-one 翻译。
x86-64 的 JIT 得益于 x86 丰富的寻址模式和 32-bit 操作的隐式零扩展,翻译最为高效。ARM64 JIT 面临 ALU32 清零的额外开销,但通过推迟 uxtw 延迟优化,在计算密集型场景中达到了接近 x86-64 的效率。
理解 JIT 的翻译策略对编写高性能 BPF 程序有两个直接价值:(1) 知道哪些 BPF 指令模式会生成低效的本地码(例如频繁的 ALU32→ALU64 切换),(2) 理解为什么某些架构上 JIT 后性能差距与 interpreter 基准不成比例。
上一篇:与验证器共舞:常见拒绝模式与编程约束(第 04 篇)
下一篇:Map 内核实现(上):hash / array / per-CPU 的数据结构与并发模型(第 06 篇)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器
eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。
【eBPF 内核实现深度拆解】BPF 指令集解码:寄存器机器、调用约定与指令编码
从 eBPF 虚拟机的 11 个 64-bit 寄存器和 struct bpf_insn 出发,逐条拆解 ALU64/ALU32、跳转、加载存储、call 四类指令的字段语义与编码格式,建立后续 verifier 和 JIT 讨论的精确基础。
【eBPF 内核实现深度拆解】验证器框架:从 BPF_PROG_LOAD 到 do_check()
跟踪 BPF_PROG_LOAD 系统调用的内核执行路径,逐层拆解 bpf_prog_load()→bpf_check()→do_check_main() 的调用链,建立 verifier 执行全景——这是理解 verifier 安全保证的入口。
【eBPF 内核实现深度拆解】Map 内核实现(上):hash / array / per-CPU 的数据结构与并发模型
从 bpf_map_ops 虚函数表出发,逐层拆解 BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_ARRAY、per-CPU 变体的内核实现——htab 的 bucket 链表与 prealloc、bpf_array 的零拷贝共享、per-CPU 分配器的无锁语义。