3. eBPF核心机制与执行模型

eBPF 之所以能在内核中安全地运行用户自定义逻辑,并不是因为它“解释执行了一段脚本”,而是因为它把程序放进了一条从字节码到机器码、从静态验证到运行时挂载的完整流水线。理解这条流水线,是后续编写 XDP、TC、kprobe 程序以及和 DPDK 协同的基础。本节围绕五个核心机制展开:BPF 指令集、验证器、JIT 编译、映射类型与程序挂载点。

一、BPF 指令集:一种受约束的寄存器虚拟机

eBPF 程序编译后是一段 BPF 字节码,运行在一个抽象的 64 位 RISC 虚拟机上。它共有 11 个寄存器:r0 用于存放返回值,r1~r5 用于传参,r6~r9 是被调用者保存寄存器,r10 是只读的栈帧指针。指令宽度固定为 8 字节,操作码采用类似 BPF_ALU64 | BPF_MOV | BPF_X 的编码方式,常见类别包括 ALU/ALU64(算术逻辑)、JMP/JMP32(跳转)、LDX/STX(内存读写)、LD(加载立即数或映射指针)以及 CALL(辅助函数调用)。

与通用 CPU 不同,BPF 虚拟机对内存访问有严格限制:程序不能直接解引用任意内核指针,只能通过验证器允许的路径访问栈、映射值、数据包数据区(如 ctx->data 到 ctx->data_end 之间)以及辅助函数返回的受信指针。栈空间默认上限为 512 字节,且必须按 8 字节对齐访问。这些约束正是安全性的来源。

二、验证器:把“不可信程序”变成“可证明安全”

验证器是 eBPF 安全模型的核心。它在程序加载时对字节码做静态分析,主要完成四件事:控制流图构建与可达性检查、寄存器类型与边界追踪、内存访问合法性检查、以及循环与指令数限制。验证器会为每个寄存器维护一个“类型状态”,例如 PTR_TO_CTX、PTR_TO_MAP_VALUE、SCALAR_VALUE,并在每条指令处推导其取值范围。

一个典型例子是数据包解析:

void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
    return XDP_PASS;
if (eth->h_proto != htons(ETH_P_IP))
    return XDP_PASS;

验证器要求先做 eth + 1 > data_end 的边界判断,之后才允许读取 eth->h_proto。如果缺少这个判断,加载会直接失败并报 invalid access to packet。因此,写 eBPF 程序时“先检查、后访问”不是风格问题,而是硬性规则。

三、JIT 编译:从字节码到本地机器码

验证通过后,内核会尝试用 JIT(Just-In-Time)编译器把 BPF 字节码翻译成目标架构的机器码,例如 x86-64 或 ARM64。JIT 将 BPF 寄存器映射到物理寄存器,把辅助函数调用转换为直接调用,并消除部分冗余指令。相比解释器逐条执行,JIT 通常能带来数倍性能提升,这也是 eBPF 能用于高速数据面的前提。

JIT 的启用状态可通过 /proc/sys/net/core/bpf_jit_enable 查看,1 表示启用,2 表示启用并输出调试信息。对于 XDP 程序,JIT 后的代码会直接挂到网卡驱动的收包路径上,避免通用网络栈开销。

四、映射类型:程序与用户态、程序与程序之间的数据通道

BPF 映射(map)是内核中的键值存储,用于在 eBPF 程序、用户态进程以及多个程序之间共享数据。常见类型包括:

  • BPF_MAP_TYPE_ARRAY:定长数组,索引即键,查找 O(1),适合配置表、统计计数。
  • BPF_MAP_TYPE_HASH:哈希表,支持任意键,适合连接跟踪、会话表。
  • BPF_MAP_TYPE_PERCPU_ARRAY / PERCPU_HASH:每 CPU 一份副本,避免多核竞争,适合高频计数。
  • BPF_MAP_TYPE_RINGBUF:多生产者单消费者环形缓冲区,用于高效向用户态上报事件。
  • BPF_MAP_TYPE_PROG_ARRAY:存放其他 BPF 程序的文件描述符,用于尾调用(tail call)实现程序链。

用户态通过 bpf() 系统调用或 libbpf 的 bpf_map_lookup_elem、bpf_map_update_elem 等接口读写映射。映射的生命周期由文件描述符引用计数管理,程序退出后若无人持有 fd,映射会被释放。

五、程序挂载点:决定程序在何处执行

同一段 eBPF 逻辑挂载到不同钩子,其上下文和可用辅助函数完全不同。常见挂载点包括:

  • XDP:挂载在网卡驱动收包路径最早处,上下文为 xdp_md,可返回 XDP_PASS、XDP_DROP、XDP_TX、XDP_REDIRECT。
  • TC(traffic control):挂载在 qdisc 层,可同时处理 ingress 和 egress,上下文为 __sk_buff。
  • kprobe / kretprobe:挂载在内核函数入口或返回处,用于追踪和观测。
  • tracepoint:挂载在内核静态追踪点,接口稳定,适合生产环境。
  • cgroup / socket:用于容器网络策略、套接字过滤等场景。

挂载方式通常通过 bpftool 或 libbpf 完成。例如把 XDP 程序挂到 eth0:

bpftool net attach xdp obj my_xdp.o dev eth0

下面这张图把上述五个机制串成一条完整的执行链,展示从 C 源码到内核中运行的程序所经过的关键阶段与数据通道。

eBPF 程序从源码到内核执行的完整链路 C 源码 SEC("xdp") 函数 clang / LLVM 编译为 BPF 字节码 验证器 Verifier 类型追踪 · 边界检查 · 控制流 JIT 编译 字节码 → x86-64 / ARM64 程序挂载点 XDP · TC · kprobe · tracepoint · cgroup 决定上下文类型与可用辅助函数 内核中运行 收包路径 / 系统调用路径 / 追踪点 BPF 映射 Map ARRAY / HASH / PERCPU RINGBUF / PROG_ARRAY 键值共享 · 尾调用跳转 用户态通过 bpf() 读写 用户态程序 libbpf / bpftool 加载 · 挂载 · 读映射 bpf_map_lookup_elem bpf_map_update_elem 编译/执行链 验证/用户态 JIT/映射 挂载点