mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2263 字
7 分钟
eBPF 虚拟机:指令集与寄存器
2026-03-18

当你写下一个 eBPF 程序并用 clang 编译后,得到的不是 x86 机器码,而是一串 eBPF 字节码——它运行在一个内核内嵌的虚拟机中。理解这个虚拟机的指令集和寄存器模型,是深入 eBPF 的必经之路:它解释了 eBPF 程序能做什么、不能做什么,以及为什么 JIT 编译后能接近原生速度。

本章将逐条解析 eBPF 指令集,拆解寄存器约定,剖析内存访问模型,并揭示 JIT 编译的内部机制。

一、eBPF 虚拟机概述#

1.1 为什么需要虚拟机#

eBPF 选择虚拟机而非直接解释执行或原生编译,有三个核心原因:

  1. 安全隔离:虚拟机提供了指令级的访问控制,验证器可以在加载时检查每条指令的合法性
  2. 可移植性:eBPF 字节码与 CPU 架构无关,同一份字节码可以在 x86、ARM、RISC-V 上运行
  3. 性能平衡:JIT 编译将热点字节码转化为本机码,在安全与性能之间取得平衡
flowchart LR SRC["C 源码"] -->|"clang -target bpf"| BPF["eBPF 字节码<br/>通用指令集"] BPF -->|"验证器检查"| VER["验证通过"] VER -->|"JIT 编译"| X86["x86 机器码"] VER -->|"JIT 编译"| ARM["ARM 机器码"] VER -->|"JIT 编译"| RISC["RISC-V 机器码"] style BPF fill:#bbdefb,stroke:#1565c0 style X86 fill:#c8e6c9,stroke:#2e7d32 style ARM fill:#fff9c4,stroke:#f9a825 style RISC fill:#ffccbc,stroke:#d84315

1.2 eBPF 与经典 BPF 的对比#

特性经典 BPF (cBPF)eBPF
指令宽度32 位64 位
寄存器2 个(A、X)10 个(r0-r9、r10)
寄存器宽度32 位64 位
指令数~20 条~130 条
寻址模式间接寻址绝对寻址 + 变址
跳转偏移16 位32 位
立即数32 位64 位
栈空间512 字节

二、eBPF 指令编码格式#

2.1 指令结构#

每条 eBPF 指令固定 8 字节,采用小端编码:

+--------+--------+--------+--------+--------+--------+--------+--------+
| op | dst:4 | src:4 | off (16 bit) | imm (32 bit) |
| (8bit) | reg | reg | | |
+--------+--------+--------+---------------------------+--------------------------+

各字段含义:

字段大小说明
op8 位操作码,包含 class、source、opcode
dst_reg4 位目标寄存器(r0-r10)
src_reg4 位源寄存器(r0-r10)
off16 位有符号偏移量(跳转/内存访问)
imm32 位有符号立即数

2.2 指令编码可视化#

8 字节指令的内部布局:

graph LR subgraph "8 字节 eBPF 指令" OP["op<br/>8 bit"] --> DST["dst_reg<br/>4 bit"] DST --> SRC["src_reg<br/>4 bit"] SRC --> OFF["off<br/>16 bit"] OFF --> IMM["imm<br/>32 bit"] end OP -.->|"class(3)"| CLASS["指令类别<br/>ALU/JMP/MEM"] OP -.->|"source(1)"| SRC_MODE["操作数来源<br/>K=立即数/X=寄存器"] OP -.->|"opcode(4)"| OPC["具体操作<br/>ADD/SUB/MOV..."] style OP fill:#bbdefb,stroke:#1565c0 style IMM fill:#c8e6c9,stroke:#2e7d32 style OFF fill:#fff9c4,stroke:#f9a825

2.3 操作码解码#

8 位操作码 op 的结构:

op = class(3) | source(1) | opcode(4)
Class含义
BPF_LD0x00加载操作(从数据包/内存加载)
BPF_LDX0x01加载到寄存器(从内存加载到 dst)
BPF_ST0x02存储(立即数写入内存)
BPF_STX0x03存储寄存器(寄存器值写入内存)
BPF_ALU0x0432 位算术逻辑运算
BPF_JMP0x0564 位跳转操作
BPF_JMP320x0632 位跳转操作(5.1+)
BPF_ALU640x0764 位算术逻辑运算

Source 字段:

Source含义
BPF_K0x00使用立即数(imm)
BPF_X0x08使用源寄存器(src_reg)

三、寄存器模型#

3.1 十个寄存器#

eBPF 虚拟机定义了 10 个 64 位寄存器:

graph TB subgraph "eBPF 寄存器调用约定" R0["r0<br/>返回值"] R1["r1<br/>参数1"] R2["r2<br/>参数2"] R3["r3<br/>参数3"] R4["r4<br/>参数4"] R5["r5<br/>参数5"] R6["r6<br/>callee-saved"] R7["r7<br/>callee-saved"] R8["r8<br/>callee-saved"] R9["r9<br/>callee-saved"] R10["r10<br/>帧指针(只读)"] end R1 & R2 & R3 & R4 & R5 -->|"Helper 调用时传入"| HELPER["Helper 函数"] HELPER -->|"返回值写入"| R0 R6 & R7 & R8 & R9 -.->|"调用后保留"| R6 style R0 fill:#c8e6c9,stroke:#2e7d32 style R1 fill:#bbdefb,stroke:#1565c0 style R2 fill:#bbdefb,stroke:#1565c0 style R3 fill:#bbdefb,stroke:#1565c0 style R4 fill:#bbdefb,stroke:#1565c0 style R5 fill:#bbdefb,stroke:#1565c0 style R6 fill:#fff9c4,stroke:#f9a825 style R7 fill:#fff9c4,stroke:#f9a825 style R8 fill:#fff9c4,stroke:#f9a825 style R9 fill:#fff9c4,stroke:#f9a825 style R10 fill:#ffcdd2,stroke:#c62828
寄存器用途调用约定
r0返回值函数返回值存于此
r1-r5函数参数传递参数给 Helper 函数,调用后不保留
r6-r9通用寄存器跨 Helper 调用保留
r10帧指针只读,指向 eBPF 栈帧起始地址

3.2 寄存器使用规则#

// eBPF 程序的寄存器使用示例
SEC("kprobe/do_sys_open")
int trace_open(struct pt_regs *ctx)
{
// r1 = ctx(第一个参数,由内核传入)
// r10 = 栈帧指针(只读)
u32 pid; // 可能使用 r6-r9 保存
struct event *e;
pid = bpf_get_current_pid_tgid() >> 32; // r0 = 返回值
// 调用 Helper 函数时:
// r1 = 第1个参数(Map 指针)
// r2 = 第2个参数(key 指针)
// r3 = 第3个参数(value 指针)
// r4 = 第4个参数(flags)
// r1-r5 在调用后被破坏
// r6-r9 在调用后保留
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
// r0 = 返回值(e 指针)
return 0; // r0 = 返回值
}
Note

r10(帧指针)是只读的——你不能修改它。eBPF 栈大小固定为 512 字节,r10 始终指向栈顶。所有栈上变量的访问都通过 r10 + offset 完成。

3.3 寄存器与 x86_64 的映射#

JIT 编译时,eBPF 寄存器映射到 x86_64 物理寄存器:

eBPF 寄存器x86_64 寄存器说明
r0rax返回值
r1rdi第1个参数
r2rsi第2个参数
r3rdx第3个参数
r4rcx第4个参数
r5r8第5个参数
r6rbx被调用者保存
r7r13被调用者保存
r8r14被调用者保存
r9r15被调用者保存
r10rbp帧指针

四、指令集详解#

4.1 ALU 指令(算术逻辑运算)#

64 位 ALU 指令(class = BPF_ALU64 = 0x07):

指令操作码语义示例
BPF_ADD0x00dst += src/immr1 += r2
BPF_SUB0x10dst -= src/immr1 -= 8
BPF_MUL0x20dst *= src/immr1 *= 3
BPF_DIV0x30dst /= src/immr1 /= r2
BPF_OR0x40dst |= src/immr1 |= 0xFF
BPF_AND0x50dst &= src/immr1 &= 0xF
BPF_LSH0x60dst <<= src/immr1 <<= 4
BPF_RSH0x70dst >>= src/imm (无符号)r1 >>= 8
BPF_NEG0x80dst = -dstr1 = -r1
BPF_MOD0x90dst %= src/immr1 %= 256
BPF_XOR0xa0dst ^= src/immr1 ^= r2
BPF_MOV0xb0dst = src/immr1 = 42
BPF_ARSH0xc0dst >>= src/imm (有符号)r1 >>>= 4

32 位 ALU 指令(class = BPF_ALU = 0x04)执行相同操作,但只操作低 32 位,高位清零。

// ALU 指令示例(eBPF C 代码 → 字节码)
// r1 = 10
// r1 += 20
// r1 *= 2
// r1 >>= 2
// 等价于 C: u64 r1 = (10 + 20) * 2 >> 2 = 15

4.2 JMP 指令(跳转操作)#

指令操作码语义
BPF_JA0x00无条件跳转:PC += off
BPF_JEQ0x10if dst == src/imm goto off
BPF_JGT0x20if dst > src/imm goto off (无符号)
BPF_JGE0x30if dst >= src/imm goto off (无符号)
BPF_JSET0x40if dst & src/imm goto off
BPF_JNE0x50if dst != src/imm goto off
BPF_JSGT0x60if dst > src/imm goto off (有符号)
BPF_JSGE0x70if dst >= src/imm goto off (有符号)
BPF_JLT0xa0if dst < src/imm goto off (无符号)
BPF_JLE0xb0if dst <= src/imm goto off (无符号)
BPF_JSLT0xc0if dst < src/imm goto off (有符号)
BPF_JSLE0xd0if dst <= src/imm goto off (有符号)
// JMP 指令示例
if (pid == 1) { // BPF_JEQ: if r1 == 1 goto +2
action = "init"; // BPF_MOV: r0 = "init"
} else { // BPF_JA: goto +1
action = "other"; // BPF_MOV: r0 = "other"
}

4.3 内存指令#

指令语义示例
BPF_LDX_MEMdst = *(size *)(src + off)r1 = *(u32 *)(r2 + 4)
BPF_ST_MEM*(size *)(dst + off) = imm*(u32 *)(r1 + 0) = 42
BPF_STX_MEM*(size *)(dst + off) = src*(u64 *)(r10 - 8) = r1
BPF_STX_XADD*(size *)(dst + off) += srcatomic add(原子加)

支持的内存访问大小:

大小说明
BPF_W0x0032 位(4 字节)
BPF_H0x0816 位(2 字节)
BPF_B0x108 位(1 字节)
BPF_DW0x1864 位(8 字节)
// 内存访问示例:从上下文读取数据
SEC("kprobe/do_sys_open")
int trace_open(struct pt_regs *ctx)
{
// 从 ctx 读取参数(内存加载)
// 等价于: r1 = *(u64 *)(ctx + 8)
// 即读取 pt_regs->di(第一个参数)
// 栈上存储临时变量
u32 key = 1; // *(u32 *)(r10 - 4) = 1
// Map 操作
u64 *val = bpf_map_lookup_elem(&my_map, &key);
// 返回值在 r0 中
}

4.4 特殊指令#

指令语义说明
BPF_CALL调用 Helper 函数imm = Helper 函数号
BPF_EXIT程序退出返回 r0 的值
BPF_ENDIAN字节序转换32/64 位大小端转换
// Helper 调用示例
// bpf_map_lookup_elem(&my_map, &key)
// 编译为:
// r1 = &my_map (Map fd)
// r2 = &key (key 指针)
// BPF_CALL bpf_map_lookup_elem (imm = 1)
// r0 = 返回值(value 指针或 NULL)

五、eBPF 栈#

5.1 栈的特性#

eBPF 程序有一个固定大小的栈空间——512 字节。这个限制是验证器强制执行的:

  • 栈通过 r10(帧指针)访问,地址从 r10 向低地址增长
  • 栈上可以存放局部变量、函数参数
  • 栈空间不可动态扩展
Warning

512 字节的栈空间非常有限!在 eBPF 程序中,你必须谨慎使用栈空间。避免在栈上分配大数组或大结构体。如果需要更大的数据存储,使用 Map。

5.2 栈使用示例#

SEC("kprobe/do_sys_open")
int trace_open(struct pt_regs *ctx)
{
// 栈上变量(通过 r10 + offset 访问)
struct event e; // 占用栈空间 sizeof(struct event)
u32 pid; // 占用栈空间 4 字节
char comm[16]; // 占用栈空间 16 字节
// 验证器会跟踪栈空间使用
// 如果总使用超过 512 字节,加载会被拒绝
pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&comm, sizeof(comm));
e.pid = pid;
__builtin_memcpy(&e.comm, comm, sizeof(comm));
bpf_ringbuf_output(&events, &e, sizeof(e), 0);
return 0;
}

六、JIT 编译#

6.1 JIT 的工作原理#

JIT(Just-In-Time)编译器将 eBPF 字节码翻译为目标架构的本机机器码,大幅提升执行效率:

flowchart LR BPF["eBPF 字节码<br/>通用指令集"] -->|"JIT 编译"| NATIVE["本机机器码<br/>x86/ARM/RISC-V"] NATIVE -->|"直接执行"| CPU["CPU 执行"] BPF -.->|"解释执行<br/>(慢)"| INTERP["eBPF 解释器"] INTERP --> CPU style NATIVE fill:#c8e6c9,stroke:#2e7d32 style INTERP fill:#ffcdd2,stroke:#c62828

6.2 JIT 编译的性能提升#

执行方式相对性能说明
解释执行1x(基准)每条指令需要一次分发
JIT 编译2-5x直接执行本机码
原生 C 代码3-8x无验证器开销

6.3 JIT 编译过程#

arch/x86/kernel/jit.c
// 内核 JIT 编译流程(简化)
struct bpf_prog *bpf_int_jit_compile(struct bpf_prog *prog)
{
// 1. 分配内存(可执行页面)
// 2. 逐条翻译 eBPF 指令为 x86 指令
// 3. 处理跳转目标地址
// 4. 刷新指令缓存
// 5. 设置 prog->bpf_func 指向 JIT 代码
// 关键优化:
// - 常量折叠(编译期计算常量表达式)
// - 死代码消除(移除不可达代码)
// - 跳转优化(短跳转替代长跳转)
}

6.4 查看 JIT 输出#

# 查看 JIT 编译后的机器码
sudo bpftool prog dump jited id 123
# 输出示例(x86_64):
# 0: push rbp
# 1: mov rbp, rsp
# 4: sub rsp, 0x200
# b: mov rdi, r1 # ctx → r1
# e: mov eax, 0x0
# 13: call 0xffffffffe0a0 # bpf_get_current_pid_tgid
# 18: shr rax, 0x20 # pid = tgid >> 32

七、尾调用与程序链接#

7.1 尾调用机制#

尾调用允许一个 eBPF 程序跳转到另一个 eBPF 程序,实现程序组合和逻辑拆分:

// 尾调用跳转表
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(max_entries, 8);
__type(key, __u32);
__type(value, __u32);
} prog_array SEC(".maps");
// 主程序
SEC("xdp")
int xdp_main(struct xdp_md *ctx)
{
// 根据协议类型跳转到不同处理程序
if (is_tcp(ctx))
bpf_tail_call(ctx, &prog_array, 0); // 跳转到 TCP 处理
else if (is_udp(ctx))
bpf_tail_call(ctx, &prog_array, 1); // 跳转到 UDP 处理
// 如果尾调用失败,继续执行默认处理
return XDP_PASS;
}
// TCP 处理程序
SEC("xdp")
int xdp_tcp(struct xdp_md *ctx)
{
// TCP 专用处理逻辑
return XDP_PASS;
}

7.2 尾调用的限制#

限制说明
最大嵌套深度8 层防止无限递归
栈不继承尾调用后原程序栈被替换
调用不返回尾调用后不回到原程序

八、指令集实战:手写 eBPF 字节码#

8.1 使用 Python 构造字节码#

#!/usr/bin/env python3
"""手写 eBPF 字节码:统计 execve 调用次数"""
import ctypes
import os
# eBPF 指令编码
class BpfInsn(ctypes.LittleEndianStructure):
_fields_ = [
("code", ctypes.c_uint8),
("dst", ctypes.c_uint8, 4),
("src", ctypes.c_uint8, 4),
("off", ctypes.c_int16),
("imm", ctypes.c_int32),
]
# 指令构造辅助
def alu64(op, dst, src=0, imm=0, off=0):
return BpfInsn(op | 0x07 | (0x08 if src else 0), dst, src, off, imm)
def mov64(dst, imm):
return alu64(0xb0, dst, imm=imm) # BPF_MOV | BPF_ALU64 | BPF_K
def exit_insn():
return BpfInsn(0x95, 0, 0, 0, 0) # BPF_EXIT
def call_insn(helper_id):
return BpfInsn(0x85, 0, 0, 0, helper_id) # BPF_CALL
# 构造简单程序:r0 = 42; return r0
prog = [
mov64(0, 42), # r0 = 42
exit_insn(), # return r0
]
# 编译为字节码
buf = b''
for insn in prog:
buf += bytes(insn)

8.2 使用 bpftool 反汇编#

# 从 .o 文件反汇编 eBPF 字节码
llvm-objdump -S -no-show-raw-insn hello.bpf.o
# 输出示例:
# hello.bpf.o: file format elf64-bpf
#
# Disassembly of section tracepoint/syscalls/sys_enter_execve:
# hello_execve:
# 0: r1 = *(u32 *)(r1 + 24)
# 1: r6 = r1
# 2: r1 = 0 ll
# 4: r1 = map_lookup_elem(r1, &r6)
# 5: if r1 == 0 goto +3
# 6: r0 = *(u64 *)(r1 + 0)
# 7: r0 += 1
# 8: *(u64 *)(r1 + 0) = r0
# 9: r0 = 0
# 10: exit

九、eBPF 指令集与 RISC-V 的渊源#

eBPF 指令集的设计深受 RISC 架构影响,特别是 RISC-V:

设计原则eBPFRISC-V
固定指令长度8 字节4 字节
Load/Store 架构
寄存器数量1032
立即数编码32 位12/20 位
分支延迟槽

eBPF 选择 Load/Store 架构意味着:所有运算都在寄存器之间进行,内存只能通过专门的加载/存储指令访问。这使得验证器可以精确追踪每个寄存器的类型和范围。

十、动手实践#

10.1 查看 eBPF 程序的字节码#

# 编译一个简单的 eBPF 程序
cat > simple.bpf.c << 'EOF'
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int simple_xdp(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
if (data + 14 > data_end) // 以太网头最小 14 字节
return XDP_PASS;
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";
EOF
clang -O2 -target bpf -c simple.bpf.c -o simple.bpf.o
# 查看字节码
llvm-objdump -S simple.bpf.o

10.2 对比 JIT 前后的代码#

# 加载程序
sudo bpftool prog load simple.bpf.o /sys/fs/bpf/simple type xdp
# 查看程序 ID
sudo bpftool prog list | grep xdp
# 查看 xlated 字节码(JIT 前)
sudo bpftool prog dump xlated id <ID>
# 查看 jited 机器码(JIT 后)
sudo bpftool prog dump jited id <ID>

10.3 使用 bpftrace 观察指令执行#

# 追踪 eBPF 程序的 Helper 调用
sudo bpftrace -e 'kprobe:bpf_helper_* { printf("helper: %s\n", func); }'
# 追踪 JIT 编译事件
sudo bpftrace -e 'kprobe:bpf_int_jit_compile { printf("JIT compile: prog=%p\n", arg0); }'

十一、本章小结#

上一章从全景视角介绍了eBPF 全景与应用场景。

主题核心要点关键词
指令编码每条指令 8 字节,包含操作码、寄存器、偏移和立即数8 字节, 操作码
寄存器模型10 个 64 位寄存器,r0 返回值、r1-r5 参数、r6-r9 保留、r10 帧指针r0-r10, 调用约定
指令分类ALU(算术逻辑)、JMP(跳转)、内存(加载/存储)、特殊(调用/退出)ALU, JMP, 内存指令
栈空间固定 512 字节,通过 r10 帧指针访问512B, 帧指针
JIT 编译将字节码编译为本机码,性能提升 2-5 倍字节码→本机码, 性能
尾调用程序间跳转机制,最多 8 层嵌套tail_call, 8 层

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

eBPF 虚拟机:指令集与寄存器
https://blog.souloss.com/posts/ebpf/ebpf-virtual-machine/
作者
Souloss
发布于
2026-03-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
eBPF 安全:LSM 与进程监控
eBPF eBPF 不仅是一种可观测性工具,更是一种安全执行机制——LSM BPF 允许在内核安全检查点挂载 eBPF 程序,实现细粒度的安全策略;Tetragon 基于 eBPF 实现实时进程监控和运行时安全。本章详解 LSM BPF 的架构、策略编写、Tetragon 的部署与使用,以及 eBPF 安全的最佳实践。
2
eBPF 与内存管理
eBPF eBPF 程序的内存使用是生产部署中的关键考量——Map 占用多少内存?eBPF 程序的栈空间有多大?容器环境中的 eBPF 内存如何计费?本章详解 eBPF 的内存模型、Map 内存开销计算、容器内存追踪、eBPF-mm 子系统,以及内存限制下的优化策略。
3
eBPF 与 WebAssembly
eBPF eBPF 提供内核可编程能力,WebAssembly 提供跨平台可移植性——两者的融合会带来什么?本章详解 Wasm-eBPF 项目、用户态 eBPF 运行时、eBPF 程序的 Wasm 封装,以及 eBPF + Wasm 在边缘计算、插件系统、跨平台可观测性中的应用前景。
4
TC:流量控制与 eBPF
eBPF TC(Traffic Control)是 Linux 内核的流量控制子系统,通过 cls_bpf 分类器可以在 TC 层挂载 eBPF 程序,实现灵活的数据包分类、修改和重定向。本章详解 TC eBPF 的架构、ingress/egress 双向处理、direct action 模式、sk_buff 操作,以及 TC 与 XDP 的选择策略。
5
eBPF 验证器:如何保证安全
eBPF eBPF 验证器是 eBPF 安全的基石——它在程序加载时进行静态分析,确保 eBPF 程序不会崩溃内核、不会越界访问内存、不会无限循环。从零讲透验证器的 DAG 验证算法、路径探索机制、安全检查规则,并解析常见的验证失败原因与修复方法。