土法炼钢兴趣小组的算法知识备份

【eBPF 内核实现深度拆解】BPF 指令集解码:寄存器机器、调用约定与指令编码

文章导航

分类入口
kernelebpf
标签入口
#ebpf#bpf-isa#bpf-insn#registers#calling-convention#bytecode#linux-kernel

目录

写 BPF 程序时,verifier 报了一个 “invalid argument” 错误,日志里只有一行 R1 type=scalar expected=ptr_to_ctx。你知道 R1 应该存放 ctx 指针,但为什么你的 R1 变成了 scalar?这个问题只有在理解了 BPF 的寄存器角色、调用约定和 struct bpf_insn 的精确编码之后,才能真正定位。

本文建立 eBPF 虚拟机的精确心智模型——不设前提。从 11 个寄存器开始,到 struct bpf_insn 的 64-bit 编码,再到四类指令的语义,最后用 bpftool 反汇编把抽象概念落到具体字节码。读完这篇,verifier 日志里的寄存器编号、类别标签、imm 字段含义将不再是谜。

一、虚拟机全景

eBPF 虚拟机是一个 64-bit RISC 寄存器机。核心规格:

下图展示了 eBPF 虚拟机的核心资源布局:

flowchart LR
    subgraph REG["寄存器文件 (11 x 64-bit)"]
        R0["R0: return value"]
        R1["R1: arg0"]
        R2["R2: arg1"]
        R3["R3: arg2"]
        R4["R4: arg3"]
        R5["R5: arg4"]
        R6["R6: callee-saved"]
        R7["R7: callee-saved"]
        R8["R8: callee-saved"]
        R9["R9: callee-saved"]
        R10["R10: frame pointer (RO)"]
    end

    subgraph STACK["BPF Stack (512 bytes)"]
        TOP["stack top (high addr)"]
        BOT["R10 - 512 = begin (low addr)"]
    end

    subgraph EXEC["执行流水线"]
        INS["bpf_insn[PC]"] --> DEC["指令解码"]
        DEC --> ALU["ALU64/ALU32"]
        DEC --> JMP["JMP/JMP32"]
        DEC --> MEM["LD/LDX/ST/STX"]
        DEC --> CALL["CALL"]
    end

    REG --> DEC
    STACK --> MEM

与 x86-64 或 ARM64 这种物理处理器不同,eBPF 的寄存器模型有以下关键差异:

  1. R10 为只读:verifier 不允许程序写入 R10。所有栈访问必须通过 R10 + offset 的方式,这确保 verifier 能精确追踪每个栈槽的状态。
  2. 调用约定固定:R1–R5 传参、R0 返回值,不因上下文类型而变化(除某些特殊的 bpf helper 返回值放入 R0 之外,还有次要返回到用户态的方式)。
  3. callee-saved 寄存器由调用者负责保存:BPF 没有 push/pop 指令,callee 必须显式用 *(u64 *)(r10 - offset) = r6r6 = *(u64 *)(r10 - offset) 来保存/恢复。

二、struct bpf_insn:64-bit 指令编码

2.1 指令格式定义

eBPF 指令的 C 定义来自内核 UAPI 头文件:

/* Linux 6.6: include/uapi/linux/bpf.h:61 */
struct bpf_insn {
    __u8  code;       /* opcode    — 8 bits */
    __u8  dst_reg:4;  /* 目标寄存器   — 4 bits */
    __u8  src_reg:4;  /* 源寄存器    — 4 bits */
    __s16 off;        /* 有符号偏移   — 16 bits */
    __s32 imm;        /* 立即数     — 32 bits */
};

64-bit 的精确布局(小端序):

Byte:  0        1         2    3    4    5    6    7
      [  code  ][dst|src][    off    ][       imm        ]
bits:   8        4   4      16               32

这是理解一切的前提。无论 ALU 操作、内存访问还是条件跳转,语义都编码在这几个字段里。关键约束:

2.2 指令类别编码

内核在 include/uapi/linux/bpf.h 中定义了指令类别的位掩码:

/* Linux 6.6: include/uapi/linux/bpf.h */
#define BPF_CLASS(code)  ((code) & 0x07)        /* 低 3 bits */
#define BPF_LD           0x00                    /* 加载(非标准) */
#define BPF_LDX          0x01                    /* 加载到寄存器 */
#define BPF_ST           0x02                    /* 从立即数存储 */
#define BPF_STX          0x03                    /* 从寄存器存储 */
#define BPF_ALU          0x04                    /* 32-bit ALU */
#define BPF_JMP          0x05                    /* 64-bit 跳转 */
#define BPF_JMP32        0x06                    /* 32-bit 跳转 */
#define BPF_ALU64        0x07                    /* 64-bit ALU */

每个类别下的 source type 由 code 的 bit 3 控制:

/* Linux 6.6: include/uapi/linux/bpf.h */
#define BPF_SRC(code)   ((code) & 0x08)         /* bit 3 */

/* ALU/JMP 操作中表示操作数来源 */
#define BPF_K           0x00                     /* 立即数(32-bit imm) */
#define BPF_X           0x08                     /* 寄存器(src_reg) */

/* ALU 操作的子类别 */
#define BPF_ALU_OP(code)    ((code) & 0xf0)     /* bits 4-7 */
#define BPF_JMP_OP(code)    ((code) & 0xf0)

将指令类别、来源类型、操作码组合后的完整示例:

助记符 code(hex) 含义
BPF_ALU64\|BPF_X\|BPF_ADD 0x0f 64-bit 加法,源操作数在寄存器中:dst += src
BPF_ALU\|BPF_K\|BPF_MOV 0xb4 32-bit MOV 立即数:dst = (u32)imm
BPF_JMP\|BPF_K\|BPF_JEQ 0x15 跳转如果 dst == imm
BPF_STX\|BPF_W\|BPF_MEM 0x63 src_reg 存为 32-bit 到 [dst_reg + off]

三、ALU 指令:计算引擎

3.1 ALU64(64-bit 操作)

BPF_ALU64 系列操作 64-bit 完整寄存器值。指令编码:

code = BPF_ALU64 | BPF_X(或BPF_K) | 具体操作码
dst = dst_reg
src = (BPF_X 时取 src_reg 的值) 或 (BPF_K 时取 imm)

完整操作码表:

/* Linux 6.6: include/uapi/linux/bpf.h */
#define BPF_ADD   0x00   /* dst += src     — 加法 */
#define BPF_SUB   0x10   /* dst -= src     — 减法 */
#define BPF_MUL   0x20   /* dst *= src     — 乘法 */
#define BPF_DIV   0x30   /* dst /= src     — 无符号除(src=0 时不更新 dst) */
#define BPF_OR    0x40   /* dst \|= src    — 按位或 */
#define BPF_AND   0x50   /* dst &= src     — 按位与 */
#define BPF_LSH   0x60   /* dst <<= src    — 逻辑左移(低 6-bit 控制移位量) */
#define BPF_RSH   0x70   /* dst >>= src    — 逻辑右移 */
#define BPF_NEG   0x80   /* dst = -dst     — 取负(无源操作数) */
#define BPF_MOD   0x90   /* dst %= src     — 无符号取模(src=0 时不更新 dst) */
#define BPF_XOR   0xa0   /* dst ^= src     — 按位异或 */
#define BPF_MOV   0xb0   /* dst = src      — 赋值 */
#define BPF_ARSH  0xc0   /* dst >>= s32(src) — 算术右移 */
#define BPF_END   0xd0   /* dst = endian(dst, imm) — 端序转换(特殊编码) */

关键语义:

3.2 ALU32(32-bit 操作)

BPF_ALU 系列的操作对象是 32-bit 子寄存器。执行 32-bit ALU 后,目标寄存器的高 32-bit 被清零。这是 BPF ISA 最关键的语义之一:

dst = (u64)((u32)dst OP (u32)src)

这意味着 BPF_ALU64BPF_ALU 的区别不仅是宽度,还影响了高 32-bit。对 JIT 编译器而言,x86-64 的 32-bit 操作自动零扩展到 64-bit(mov eax, eax 就清零了 rax 高 32-bit),但 ARM64 需要显式的 uxtw 指令。

3.3 端序转换指令

BPF_END 是 ALU 操作中编码方式最特殊的一个:

/* code = BPF_ALU (或 BPF_ALU64) | BPF_K | BPF_END */
/* imm 字段编码:高位指定源字节序和目标字节序,低位指定宽度 */

#define BPF_TO_LE    0x00   /* 转换为小端 */
#define BPF_TO_BE    0x08   /* 转换为大端 */

/* 宽度指定在最下层 */
#define BPF_IMM_SIZE(imm)  ((imm) & 0x60)

#define BPF_W    0x00   /* 32-bit 转换 */
#define BPF_H    0x08   /* 16-bit 转换 */
#define BPF_B    0x10   /* 8-bit 转换 */
#define BPF_DW   0x18   /* 64-bit 转换(LDX/STX 等) */

示例:BPF_END | BPF_TO_BE | BPF_H 将目标寄存器的低 16-bit 从主机字节序转换为大端。这条指令用于跨平台数据解析(网络字节序是大端,x86 是小端)。

3.4 原子操作:BPF_ATOMIC

BPF_ATOMICBPF_STX 的一个特殊子类别(code = BPF_STX | BPF_W | BPF_ATOMIC),使用 imm 字段指定原子操作类型:

/* Linux 6.6: include/uapi/linux/bpf.h */
#define BPF_ATOMIC_OP(imm)  ((imm) & 0x70)   /* 操作类型(bits 4-6) */
#define BPF_ATOMIC_FETCH(imm) ((imm) & 0x80) /* 是否返回旧值(bit 7) */

#define BPF_ADD    0x00   /* 原子加 */
#define BPF_AND    0x10   /* 原子按位与 */
#define BPF_OR     0x20   /* 原子按位或 */
#define BPF_XOR    0x30   /* 原子按位异或 */
#define BPF_XCHG   0x40   /* 原子交换(无条件交换,不受 FETCH 影响) */
#define BPF_CMPXCHG 0x50  /* 原子比较交换(R0 包含旧值,R1 包含 compare 值) */

语义:对 [dst_reg + off] 指向的内存执行原子 src_reg OP value 操作。如果 BPF_FETCH 置位,操作前的内存值写回 src_regBPF_CMPXCHG 使用隐式寄存器约定:R0 包含旧值,R1 包含 compare 值,只有 [dst_reg + off] == R0 时才会将内存更新为 src_reg 的内容。

verifier 对原子操作有严格的对齐要求——操作宽度必须与内存自然对齐匹配(16-bit 操作需要 2-byte 对齐,32-bit 需要 4-byte,64-bit 需要 8-byte)。未对齐访问将被 verifier 拒绝。

四、内存指令:加载与存储

4.1 加载指令

eBPF 有两种加载指令:

BPF_LDX(标准加载):将内存中的值加载到目标寄存器。

dst = *(size *)(src + off)
编码:code = BPF_LDX | BPF_{W,H,B,DW} | BPF_MEM
     dst_reg = 目标寄存器
     src_reg = 基址寄存器
     off = 偏移
size:BPF_W(32-bit)、BPF_H(16-bit)、BPF_B(8-bit)、BPF_DW(64-bit)

BPF_LD(特殊加载):主要用于加载 64-bit 立即数和访问上下文内置字段。

/* BPF_LD_IMM64: 加载 64-bit 立即数(占用两条指令) */
/* 指令 0: code=BPF_LD|BPF_DW|BPF_IMM, dst_reg, imm=低32-bit */
/* 指令 1: code=0x00 (保留), imm=高32-bit */

BPF_LD_IMM64 是 eBPF ISA 中唯一的双宽指令,占用 16 字节。这两条指令必须连续出现,verifier 会在第二条指令上做完整性检查。如果第二条指令的 code 不是 0x00,程序被拒绝。

4.2 存储指令

BPF_STX(寄存器到内存)*(size *)(dst + off) = src

编码:code = BPF_STX | BPF_{W,H,B,DW} | BPF_MEM
     dst_reg = 基址寄存器
     src_reg = 值寄存器
     off = 偏移

BPF_ST(立即数到内存)*(size *)(dst + off) = imm

编码:code = BPF_ST | BPF_{W,H,B,DW} | BPF_MEM
     dst_reg = 基址寄存器
     imm = 立即数值
     off = 偏移

注意 BPF_ST 没有 src_reg 字段——立即数从 imm 读取。

4.3 存储宽度约束

大小 常量 位数 对齐
8-bit BPF_B 8 任意
16-bit BPF_H 16 2-byte
32-bit BPF_W 32 4-byte
64-bit BPF_DW 64 8-byte

BPF_ST 不支持 BPF_DW(64-bit 立即数存储改为 BPF_LD_IMM64 + BPF_STX 组合)。

4.4 栈访问模式

BPF 程序的所有局部变量都在 512-byte 栈上。访问模式:

*(u64 *)(r10 - 8) = r0      ; BPF_STX_DW BPF_REG_10, BPF_REG_0, -8
r1 = *(u32 *)(r10 - 16)     ; BPF_LDX_W  BPF_REG_1, BPF_REG_10, -16

栈从高地址向低地址增长。R10 + 0 是栈顶(进入函数时的 SP),向下 512 字节为可用区域。R10 - 512 是栈底。任何超出此范围的访问被 verifier 的 check_stack_access_within_bounds() 拒绝。

4.5 数据包/上下文访问

XDP 和 socket filter 程序的 ctx 是指向包的受限指针。访问必须通过边界检查:

r1 = ctx->data          ; BPF_LDX_DW r1, [r1(=ctx) + data_offset]
r2 = ctx->data_end      ; BPF_LDX_DW r2, [r1 + data_end_offset]
; 边界检查(必须有)
if r1 + 4 > r2 goto +1 ; BPF_JGE r1, r2  -> 错误路径
r3 = *(u32 *)(r1 + 0)   ; 现在可以安全地读 4 字节

verifier 在 check_mem_access() 中强制执行此模式——任何 PTR_TO_PACKET 的 dereference 必须伴随有效的边界检查(详见第 04 篇的模式 2)。

五、跳转指令:控制流

5.1 无条件跳转

BPF_JA(无条件跳转):

code = BPF_JMP | BPF_JA (= 0x05)
PC += off + 1

off 是相对偏移量(从下一条指令算起,偏移 0 代表继续执行下一条即空跳转)。BPF 跳转只能在静态已知的指令边界上移动。BPF_JA 的 forward/backward 均被允许,但 verifier 会检查跳转目标是否在有效指令范围内(check_jmp_op())。

5.2 条件跳转(BPF_JMP 和 BPF_JMP32)

条件跳转的形式为:

code = BPF_JMP(或BPF_JMP32) | BPF_X(或BPF_K) | 比较操作码
if dst OP src: PC += off + 1     ; 条件为真时跳过 off 条指令
else:         PC += 1            ; 继续顺序执行

完整比较操作码:

/* Linux 6.6: include/uapi/linux/bpf.h */
#define BPF_JEQ   0x10   /* == (等于) */
#define BPF_JGT   0x20   /* >  (大于,无符号) */
#define BPF_JGE   0x30   /* >= (大于等于,无符号) */
#define BPF_JSET  0x40   /* &  (按位与不等于零) */
#define BPF_JNE   0x50   /* != (不等于) */
#define BPF_JSGT  0x60   /* >  (大于,有符号) */
#define BPF_JSGE  0x70   /* >= (大于等于,有符号) */
#define BPF_JLT   0xa0   /* <  (小于,无符号) */
#define BPF_JLE   0xb0   /* <= (小于等于,无符号) */
#define BPF_JSLT  0xc0   /* <  (小于,有符号) */
#define BPF_JSLE  0xd0   /* <= (小于等于,有符号) */

BPF_JMPBPF_JMP32 的唯一区别是比较的位宽:

BPF_JSET 的操作:if (dst & src) goto PC+off+1,用于高效的位掩码测试。

5.3 程序出口指令

#define BPF_EXIT  0x90   /* 从 BPF 程序返回 */

/* code = BPF_JMP | BPF_EXIT = 0x95 */
/* R0 包含返回值 */

BPF_EXIT 没有目标和源操作数,必须出现在程序流图中。verifier 通过 check_cfg() 确保所有路径都能到达某个 BPF_EXIT(无死循环、无不可达指令);一个程序可以有多条分支各自以 BPF_EXIT 结束,但每条路径都必须能证明会终止于 EXIT,不能在中途用 EXIT 以外的机制“提前返回”。

5.4 函数调用指令

#define BPF_CALL  0x80   /* 函数调用 */
/* code = BPF_JMP | BPF_CALL = 0x85 */

/* Helper 函数调用:imm = helper function ID (负数或正数) */
/* 同一程序内调用:imm = BPF_PSEUDO_CALL (= 1),off = 目标指令偏移 */

BPF_CALL 有两种模式,由 src_reg 字段区分:

src_reg 含义 imm 说明
0 Helper 函数调用(BPF_PSEUDO_KERNEL_CALL imm = helper 函数 ID(如 bpf_map_lookup_elem = 1)
BPF_PSEUDO_CALL 程序内调用(BPF_PSEUDO_CALL = 1) imm = 1,off = 目标子程序的起始指令偏移
BPF_PSEUDO_KFUNC_CALL 内核函数调用(kfunc,5.x+) imm = 内核 BTF 函数 ID

验证器行为差异:

六、调用约定:寄存器角色

eBPF 的调用约定定义了 11 个寄存器在函数调用中的行为。以下表总结了完整约定:

寄存器 角色 调用者保存? 约束
R0 返回值 是(调用覆盖) 存放 helper 调用或程序退出时的返回值
R1 第 1 参数 BPF_PROG_LOAD 时为 ctx 指针;helper 调用时为第 1 参数
R2 第 2 参数 helper 调用参数
R3 第 3 参数 helper 调用参数
R4 第 4 参数 helper 调用参数
R5 第 5 参数 helper 调用参数
R6 callee-saved 否(调用保留) BPF_PSEUDO_CALL 保持值
R7 callee-saved 同上
R8 callee-saved 同上
R9 callee-saved 同上
R10 帧指针(只读) 否(保持不变) 始终指向栈顶,禁止写入

BPF_PROG_LOAD 时的初始寄存器状态(每种程序类型略有不同):

寄存器 初始值 来源
R1 ctx 指针 内核在调用 BPF 程序时自动设置
R2–R5 未初始化 NOT_INIT
R6–R9 未初始化 NOT_INIT
R10 帧指针 初始化为 BPF 栈帧地址
R0 未初始化 NOT_INIT

Helper 函数调用时的参数传递

R1=arg0, R2=arg1, R3=arg2, R4=arg3, R5=arg4
调用 helper 后:R1--R5 被标记为 NOT_INIT(调用者不能再信任它们的值)
R0 被 helper 填充返回值(类型取决于 helper——PTR_TO_MAP_VALUE, SCALAR_VALUE 等)

程序内调用(BPF_PSEUDO_CALL 的约定:

调用者必须显式将参数放在 R1–R5 中。callee 的 R6–R9 保留调用前的值。callee 对 R6–R9 的修改会反映到返回后的调用者(因为它们是同一个寄存器文件)。如果 callee 需要修改 R6–R9,必须在入口处保存到自己的栈帧,退出前恢复。

verifier 对程序内调用的检查在 check_func_call() 中——它创建一个新的 bpf_verifier_state,分支进入被调用函数,验证完成后将状态合并回调用者。被调用函数的 R0 返回类型和范围会传递回调用者的 R0。

七、从字节码到反汇编

7.1 获取字节码

使用 bpftool 查看任何已加载程序的字节码和反汇编:

# 查看程序的原始字节码(xlated = 翻译后的指令)
bpftool prog dump xlated id <PROG_ID>

# 查看带操作码的详细输出
bpftool prog dump xlated id <PROG_ID> opcodes

# 查看 JIT 编译后的本机码
bpftool prog dump jited id <PROG_ID>

7.2 反汇编示例

以下是一个从 bpf_map_lookup_elem 开始的最小 BPF 程序的反汇编(xlated 模式):

# bpftool prog dump xlated id 42 opcodes
   0: (b7) r1 = 0x0               ; BPF_ALU64_IMM(BPF_MOV, R1, 0)
   1: (63) *(u32 *)(r10 -4) = r1  ; BPF_STX(BPF_W, R10, R1, -4)
   2: (b7) r2 = 0x4               ; BPF_ALU64_IMM(BPF_MOV, R2, 4)
   3: (bf) r3 = r10               ; BPF_ALU64_REG(BPF_MOV, R3, R10)
   4: (07) r3 += -4               ; BPF_ALU64_IMM(BPF_ADD, R3, -4)
   5: (18) r1 = map[id:1]         ; BPF_LD_IMM64(R1, map_fd)  [双宽指令]
   7: (85) call bpf_map_lookup_elem#1  ; BPF_CALL(1)
   8: (15) if r0 == 0x0 goto pc+1 ; BPF_JMP_IMM(BPF_JEQ, R0, 0, 1)
   9: (b7) r0 = 0x0               ; BPF_ALU64_IMM(BPF_MOV, R0, 0)
  10: (95) exit                    ; BPF_EXIT

逐条解码:

7.3 端序转换的实际字节码

# 从大端 16-bit 转换为主机字节序
0: (dc) r0 = be16 r0     ; BPF_ALU|BPF_K|BPF_END, dst=R0, imm=BPF_TO_LE|BPF_H

0xdc 的解码:BPF_ALU(0x04) | BPF_K(0x00) | BPF_END(0xd0) = 0xd4,但实际的编码是 BPF_ALU|BPF_K|BPF_END = 0xdcBPF_END 在 opcode 位置时 0xd0|0x0c = 0xdc,其中 0x0c 来自 imm 的编码位)。

八、Mermaid:从 C 源码到本地执行的完整流程

flowchart TB
    subgraph COMPILE["编译阶段(用户态)"]
        C["C/BPF 源码<br/>e.g., xdp_filter.c"]
        CLANG["clang -target bpf -O2<br/>-g (BTF 调试信息)"]
        ELF["BPF 目标文件 (.o)<br/>ELF sections: .text, maps, .BTF, .BTF.ext"]
    end

    subgraph LOAD["加载阶段(用户态→内核)"]
        LIBCALL["libbpf / bpftool"]
        SYSCALL["bpf(BPF_PROG_LOAD, ...)"]
    end

    subgraph VERIFY["验证阶段(内核)"]
        PARSER["指令流解析<br/>struct bpf_insn[]"]
        CFG["控制流图构建<br/>check_cfg()"]
        DOCHECK["逐条模拟执行<br/>do_check() 主循环"]
        VERDICT["验证判决<br/>ACCEPT / REJECT"]
    end

    subgraph JIT_COMP["JIT 编译阶段(内核)"]
        JIT_ENTRY["bpf_jit_compile()<br/>架构调度"]
        X86["x86-64 JIT<br/>bpf_int_jit_compile()"]
        ARM64["ARM64 JIT<br/>bpf_int_jit_compile()"]
        JIT_IMAGE["JIT 镜像<br/>rwx kernel memory"]
    end

    subgraph EXEC["执行阶段"]
        HOOK["attach 到 hook<br/>kprobe/XDP/TC/socket"]
        NATIVE["本地指令执行<br/>native x86/ARM 码"]
        MAPS["BPF maps<br/>内核态状态共享"]
    end

    C --> CLANG --> ELF --> LIBCALL --> SYSCALL
    SYSCALL --> PARSER --> CFG --> DOCHECK --> VERDICT
    VERDICT -->|ACCEPT| JIT_ENTRY
    VERDICT -->|REJECT| REJ["返回错误 + verifier log"]
    JIT_ENTRY --> X86
    JIT_ENTRY --> ARM64
    X86 --> JIT_IMAGE
    ARM64 --> JIT_IMAGE
    JIT_IMAGE --> HOOK --> NATIVE
    NATIVE <--> MAPS

在这个流程中,本文覆盖的是验证阶段中的「指令解码」部分——即 struct bpf_insn[] 到具体指令语义的映射。verifier 的 do_check() 主循环(第 02 篇)和 JIT 的架构翻译(第 05 篇)是这个流水线的下游。

九、与后续篇章的关系

本文建立的寄存器角色和指令编码模型是后续所有讨论的基础:

十、小结

eBPF 指令集的本质是一个简单的 64-bit RISC 寄存器机。11 个寄存器各有固定角色,每条指令 64-bit,编码了操作码、寄存器、偏移和立即数四个字段。这不是设计上的”简单粗暴”——这是有意为之:verifier 需要分析的指令类型越少,安全证明就越可行。固定宽度指令消除了变长指令带来的对齐和解析问题;有限的寄存器集合(11 个而非 32 个)约束了 verifier 需要跟踪的状态空间;固定的调用约定使得 check_helper_call() 的参数验证成为机械的查表操作而非复杂的推断过程。

当你在 bpftool prog dump xlated 中看到 (bf) r3 = r10 这样的输出时,你看到的不是”某种魔法字节码”——而是一个精确的 bpf_insn 结构体正在被 verifier 逐条解码和执行。

参考

内核源码(A 级,Linux 6.6)

规范(A 级)


下一篇验证器框架:从 BPF_PROG_LOAD 到 do_check()(第 02 篇)

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .