1 BPF Instruction Set Specification, v1.0 — The Linux Kernel documentation

寄存器和调用约定#

eBPF有10个通用寄存器和一个只读的帧指针寄存器,所有的寄存器都是64bits位长。

eBPF的调用约定如下:

  • R0: 函数调用的返回值和eBPF程序的退出值
  • R1 - R5: 函数调用的参数
  • R6 - R9: callee saved registers,由函数调用负责保存到栈上
  • R10: 只读的帧指针,用于访问栈 R0 - R5是临时寄存器,eBPF程序在函数调用时如需使用需要将它们保存/填充。

指令编码#

eBPF使用64bit指令,编码如下:

32位(最高有效位) 16位 4位 4位 8位(最低有效位)
immediate offset source register dest register opcode
  • imm: 有符号整型立即数
  • offset: 有符号整型数,用于指针算术
  • src_reg: 除开特殊指令的编码外(如64位立即数指令)指的是源寄存器(R0 - R10)
  • dst_reg: 目标寄存器(R0 - R10)
  • opcode: 操作码

绝大多数指令不会使用所有的字段。没有使用的字段应当被置0。多字节的字段(如imm和offset)在大端BPF中以大端字节序存储,小端BPF中以小端字节序存储。

opcode                  offset imm          assembly
       src_reg dst_reg
07     0       1        00 00  44 33 22 11  r1 += 0x11223344 // little
       dst_reg src_reg
07     1       0        00 00  11 22 33 44  r1 += 0x11223344 // big

除了这些基本指令编码外,eBPF还有一种宽指令编码,在基本指令之后使用第2个64位立即数做扩展。第2个立即数包括一个伪指令,和基本指令的格式一样,不过所有的字段(除了imm)都置为0。它的imm作为整个宽指令中的imm64值的高32位:

basic_instruction
.-----------------------------.
|                             |
code:8 regs:8 offset:16 imm:32 unused:32 imm:32
                               |              |
                               '--------------'
                              pseudo instruction
                              
imm64 = (next_imm << 32) | imm

指令类型#

opcode中最低3位有效位表示指令的类型,如下表所示:

类型 含义
BPF_LD 0x00 非标准load操作
BPF_LDX 0x01 加载到寄存器的操作
BPF_ST 0x02 存储立即数的值到内存中
BPF_STX 0x03 存储寄存器中的值到内存中
BPF_ALU 0x04 32位算术操作
BPF_JMP 0x05 64位跳转操作
BPF_JMP32 0x06 32位跳转操作
BPF_ALU64 0x07 64位算术操作

算术和跳转指令#

对于算术和跳转指令(BPF_ALU, BPF_ALU64, BPF_JMP, BPF_JMP32),8位的opcode字段可以分成3个部分:

4位(最高有效位) 1位 3位(最低有效位)
操作码 源操作数 指令类型
第4位的源操作数编码如下:
含义
BPF_K 0x00 使用32位立即数作为源操作数
BPF_X 0x08 使用src_reg寄存器作为源操作数
前4个最高有效位存储了操作码。

算术指令#

对于相同的操作,BPF_ALU使用32位宽的操作数,而BPF_ALU64使用64位宽的操作数。操作码编码的操作如下:

操作码 offset 含义
BPF_ADD 0x00 0 dst += src
BPF_SUB 0x10 0 dst -= src
BPF_MUL 0x20 0 dst *= src
BPF_DIV 0x30 0 dst = (src != 0) ? (dst / src) : 0
BPF_SDIV 0x30 1 dst = (src != 0) ? (dst s/ src) : 0
BPF_OR 0x40 0 dst |= src
BPF_AND 0x50 0 dst &= src
BPF_LSH 0x60 0 dst «= (src & mask)
BPF_RSH 0x70 0 dst »= (src & mask)
BPF_NEG 0x80 0 dst = -dst
BPF_MOD 0x90 0 dst = (src != 0) ? (dst % src) : dst
BPF_SMOD 0x90 1 dst = (src != 0) ? (dst s% src) : dst
BPF_XOR 0xa0 0 dst ^= src
BPF_MOV 0xb0 0 dst = src
BPF_MOXSX 0xb0 8/16/32 dst = (s8,s16,s32)src
BPF_ARSH 0xc0 0 sign extending dst »= (src & mask)
BPF_END 0xd0 0 字节序转换

在算术运算过程中允许下溢和上溢,意味着64位或32位的值会进行循环。如果BPF程序执行会导致除以零,目标寄存器将被设置为零。如果执行会导致模除零,对于BPF_ALU64指令,目标寄存器的值保持不变,而对于BPF_ALU指令,目标寄存器的高32位将被清零。

对于无符号操作(BPF_DIV和BPF_MOD),对于BPF_ALU指令,imm被解释为32位无符号值。对于BPF_ALU64指令,imm首先被符号扩展为64位,然后被解释为64位无符号值。

对于有符号操作(BPF_SDIV和BPF_SMOD),对于BPF_ALU指令,imm被解释为32位有符号值。对于BPF_ALU64指令,imm首先被符号扩展为64位,然后被解释为64位有符号值。

当被除数或除数为负数时,有关有符号模除操作的定义存在差异,不同的编程语言(如Python、Ruby等)与C、Go、Java等实现可能不同。该规范要求使用截断除法(例如,在C、Go等中实现的方式)来进行有符号模除操作(例如,-13 % 3 == -1)。

a % n = a - n * trunc(a / n)

BPF_MOVSX指令执行带符号扩展的移动操作。BPF_ALU | BPF_MOVSX将8位和16位操作数进行符号扩展,转换为32位操作数,并将剩余的高32位清零。BPF_ALU64 | BPF_MOVSX将8位、16位和32位操作数进行符号扩展,转换为64位操作数。

移位操作在64位操作中使用0x3F(63)的掩码,在32位操作中使用0x1F(31)的掩码。

举例:

// BPF_ADD|BPF_X|BPF_ALU
dst_reg = (u32) dst_reg + (u32) src_reg;

// BPF_ADD|BPF_X|BPF_ALU64
dst_reg = dst_reg + src_reg

// BPF_XOR|BPF_K|BPF_ALU
src_reg = (u32) src_reg ^ (u32) imm32

// BPF_XOR|BPF_K|BPF_ALU64
src_reg = src_reg ^ imm32

字节序转换指令#

字节序转换指令使用BPF_ALU和BPF_ALU64指令类加上BPF_END,仅对目标寄存器进行操作,不需要源寄存器和imm中的值。

对于BPF_ALU指令类,操作码中的1位源操作数字段用于选择操作转换的字节顺序(从哪种字节顺序转换到哪种字节顺序)。对于BPF_ALU64指令,操作码中的1位源操作数字段保留,必须设置为0。如下所示:

指令类型 源操作数 含义
BPF_ALU BPF_TO_LE 0x00 在主机字节序和小端字节序间转换
BPF_ALU BPF_TO_BE 0x08 在主机字节序和大端字节序间转换
BPF_ALU64 Reserved 0x00 反向字节序转换
imm字段编码了交换操作的位宽,支持16, 32, 64。
// BPF_ALU | BPF_TO_LE | BPF_END, host-endian format to little-endian format
dst = htole16(dst) // imm = 16
dst = htole32(dst) // imm = 32
dst = htole64(dst) // imm = 64

// BPF_ALU | BPF_TO_BE | BPF_END, host-endian format to big-endian format
dst = htobe16(dst) // imm = 16
dst = htobe32(dst) // imm = 32
dst = htobe64(dst) // imm = 64

// BPF_ALU64 | BPF_TO_LE | BPF_END, big- or little-endian to opposite endianness
dst = bswap16(dst) // imm = 16
dst = bswap32(dst) // imm = 32
dst = bswap64(dst) // imm = 64

跳转指令#

同样,BPF_JMP32使用32位宽的操作数,BPF_JMP使用64位宽的操作数。操作码编码如下:

操作码 源操作数 含义 注释
BPF_JA 0x00 0x0 PC += off BPF_JMP 指令类
BPF_JA 0x00 0x0 PC += imm BPF_JMP32 指令类
BPF_JEQ 0x10 any PC += off if dst == src
BPF_JGT 0x20 any PC += off if dst > src unsigned
BPF_JGE 0x30 any PC += off if dst >= src unsigned
BPF_JSET 0x40 any PC += off if dst & src
BPF_JNE 0x50 any PC += off if dst != src
BPF_JSGT 0x60 any PC += off if dst > src signed
BPF_JSGE 0x70 any PC += off if dst >= src signed
BPF_CALL 0x80 0x0 通过地址调用辅助函数 Helper functions
BPF_CALL 0x80 0x1 调用 PC += imm Program-local functions
BPF_CALL 0x80 0x2 通过BTF ID调用辅助函数 Helper functions
BPF_EXIT 0x90 0x0 函数/程序返回 BPF_JMP only
BPF_JLT 0xa0 any PC += off if dst < src unsigned
BPF_JLE 0xb0 any PC += off if dst <= src unsigned
BPF_JSLT 0xc0 any PC += off if dst < src signed
BPF_JSLE 0xd0 any PC += off if dst <= src signed
eBPF程序需要在做BPF_EXIT操作之前将返回值存储到R0。

举例:

// BPF_JSGE | BPF_X | BPF_JMP32 (0x7e)
if (s32) dst s>= (s32)src goto +offset // 's>=' indicates a signed '>=' comparison

// BPF_JA | BPF_K | BPF_JMP32(0x06)
gotol +imm // 'imm' means the branch offset comes from insn 'imm' field

对于BPF_JA指令的两种变体,BPF_JMP类允许由offset字段指定的16位跳转偏移量,而BPF_JMP32类允许由imm字段指定的32位跳转偏移量。大于16位的条件跳转可以转换为小于16位的条件跳转加上一个32位的无条件跳转。

早期的指令集里面没有BPF_JMP32的变体,这也可能是早期BPF程序限制指令数的原因?

早期的指令集里面BPF_CALL也只有通过imm中编码的地址调用辅助函数这一种变体,每个BTF程序类型可用的helper函数可能有所不同,但地址值在所有程序类型中是唯一的。现在加上新的2种变体后,BPF也就支持了本地函数调用和BTF ID识别辅助函数调用。

本地函数调用类似于BPF_JA,通过从调用指令开始的偏移量进行引用。偏移量编码在调用指令的imm字段中。本地函数执行完成后执行BPF_EXIT指令返回调用方。

加载和存储操作#

对于加载和存储操作(BPF_LD, BPF_LDX, BPF_ST, BPF_STX),8位的opcode字段被分成3个部分:

3位(最高有效位) 2位 3位(最低有效位)
模式(mode) 大小(size) 指令类型
size取值如下:
size 含义
BPF_W 0x00 word(4字节)
BPF_H 0x08 half word(2字节)
BPF_B 0x10 1字节
BPF_DW 0x18 double word(8字节)
mode取值如下:
mode 含义
BPF_IMM 0x00 64位立即数指令
BPF_ABS 0x20 从cBPF继承的数据包访问指令(absolute),已废弃
BPF_IND 0x40 从cBPF继承的数据包访问指令(indirect),已废弃
BPF_MEM 0x60 常规的加载和存储指令
BPF_MEMSX 0x80 带符号扩展的加载指令
BPF_ATOMIC 0xc0 原子指令

常规加载和存储指令#

BPF_MEM用于编码在寄存器和内存之间传输数据的常规加载和存储指令。如:

// BPF_MEM|<size>|BPF_STX
*(size *) (dst_reg + off) = src_reg

// BPF_MEM|<size>|BPF_ST
*(size *) (dst_reg + off) = imm32

// BPF_MEM|<size>|BPF_LDX
dst_reg = *(unsigned size *) (src_reg + off)

其中,size是BPF_B, BPF_H, BPF_W, BPF_DW其中之一,unsigned size是u8, u16, u32 or u64其中之一。

带符号扩展的加载指令#

BPF_MEMSX用于编码带符号扩展的加载指令,用于在寄存器和内存之间传输数据,如:

// BPF_MEMSX | <size> | BPF_LDX
dst = *(signed size *) (src + offset)

其中,size是BPF_B, BPF_H, BPF_W其中之一,unsigned size是s8, s16, s32其中之一。

原子指令#

eBPF支持的所有原子操作都被编码为使用BPF_ATOMIC模式修饰符的存储操作,如下:

  • 32位原子操作:BPF_ATOMIC | BPF_W | BPF_STX
  • 64位原子操作:BPF_ATOMIC | BPF_DW | BPF_STX
  • 不支持8位和16位的原子操作

立即数(immediate)字段在原子指令中做了额外的编码, 用于表示实际的操作类型:

imm 含义
BPF_ADD 0x00 atomic add
BPF_OR 0x40 atomic or
BPF_AND 0x50 atomic and
BPF_XOR 0xa0 atomic xor
BPF_FETCH 0x01 修饰符:返回原值
BPF_XCHG 0xe0 | BPF_FETCH atomic exchange
BPF_CMPXCHG 0xf0 | BPF_FETCH atomic compare and exchange
前面4个为简单原子操作,BPF_FETCH是修饰符,后面2个为复杂原子操作。BPF_FETCH对于简单原子操作来说是可选的,对复杂原子操作是始终置位的。如果BPF_FETCH被置位,操作也会用内存被修改之前的值替换源寄存器中的值。

举例:

// .imm = BPF_ADD, .code = BPF_ATOMIC|BPF_W|BPF_STX
lock xadd *(u32 *)(dst_reg + off16) += src_reg

// .imm = BPF_ADD, .code = BPF_ATOMIC | BPF_DW | BPF_STX
lock xadd *(u64 *)(dst_reg + off16) += src_reg

dst_reg + off指向的内存地址中的值被原子修改,src_reg中的值作为另外一个操作数。

复杂原子操作:

  • BPF_XCHG: 原子性地交换src_reg和dst_reg + off指向内存地址的值
  • BPF_CMPXCHG: 原子性地比较dst_reg + off指向内存地址的值和R0。如果匹配则用src_reg替换该值。无论哪种情况,原来的值都会被零扩展并加载回R0寄存器中。

当-mcpu=v3使能时,Clang默认能生成原子指令。如果低版本的-mcpu设置时,唯一能被Clang生成的原子指令是不带BPF_FETCH的BPF_ADD。如果需要在保持较低-mcpu版本的同时启用原子特性,可以使用-Xclang -target-feature -Xclang +alu32选项。

64位立即数指令#

64位立即数指令使用宽指令编码,使用BPF_IMM模式修饰符,并且使用基本指令中的源寄存器字段存储操作码的子类型。

下面是BPF_LD | BPF_DW | BPF_IMM(0x18)指令的子类型:

源寄存器值 伪代码 立即数类型 目的寄存器值类型
0x0 dst = imm64 integer integer
0x1 dst = map_by_fd(imm) map fd map
0x2 dst = map_val(map_by_fd(imm)) + next_imm map fd data pointer
0x3 dst = var_addr(imm) variable id data pointer
0x4 dst = code_addr(imm) integer code pointer
0x5 dst = map_by_idx(imm) map index map
0x6 dst = map_val(map_by_idx(imm)) + next_imm map index data pointer
  • map_by_fd(imm): 将一个32位的文件描述符转换成一个map的地址
  • map_by_idx(imm): 将一个32位的index转换成一个map的地址
  • map_val(map): 获取给定map的第一个值的地址
  • var_addr(imm): 获取给定id的platform variable的地址
  • code_addr(imm): 获取指定相对偏移量处的指令的地址
  • 立即数类型能够被反汇编器用作展示
  • 目的寄存器值类型能够被用作校验和JIT编译

Maps#

Maps是一些平台上可被BPF程序访问的共享内存区域。不同的map类型可能存在或不存在单个连续的内存区域。当前的map_val(map)仅对具有单个连续内存区域的map类型有意义。

如果平台支持,每个map都有一个文件描述符(fd)。map_by_fd(imm)表示获取具有指定文件描述符的map。BPF程序在加载时可以定义程序中需要使用的一组maps,map_by_idx(imm)表示获取包含该指令的BPF程序中的这组maps中给定索引的map。

Platform Variables#

平台变量是由运行时公开并可被某些平台上的BPF程序访问的内存区域,通过整数ID进行标识。var_addr(imm)操作表示获取由给定ID标识的内存区域的地址。

老的规范(v5.17)中只有64位立即数指令只有第一个子类型,其它的都是后来添加的。

数据包访问指令#

eBPF有两个非通用指令,用于访问数据包数据:

  • (BPF_ABS | <size> | BPF_LD)
  • (BPF_IND | <size> | BPF_LD)

这些指令必须从cBPF继承下来,用来保证在eBPF解释器中运行的套接字过滤器具有强大的性能。这些指令只能在解释器上下文为指向struct sk_buff的指针时使用,并具有七个隐式操作数。寄存器R6是一个隐式输入,必须包含指向sk_buff的指针。寄存器R0是一个隐式输出,其中包含从数据包中获取的数据。寄存器R1-R5是临时寄存器,不能用于在BPF_ABS | BPF_LD或BPF_IND | BPF_LD指令之间存储数据。

这些指令还具有隐式的程序退出条件。当eBPF程序尝试访问数据包边界之外的数据时,解释器将中止程序的执行。JIT编译器也必须保留此属性。src_reg和imm32字段是这些指令的显式输入。

这两个指令在 spec 1.0 中已经被废弃。

简单的例子#

原始c语言代码:

// BPF_IND|BPF_W|BPF_LD
R0 = ntohl(*(u32 *) (((struct sk_buff *) R6)->data + src_reg + imm32))

并且R1至R5的值会被该操作覆盖。

//go:build ignore

#include "common.h"

char __license[] SEC("license") = "Dual MIT/GPL";

struct bpf_map_def SEC("maps") kprobe_map = {
        .type        = BPF_MAP_TYPE_ARRAY,
        .key_size    = sizeof(u32),
        .value_size  = sizeof(u64),
        .max_entries = 1,
};

SEC("kprobe/sys_execve")
int kprobe_execve() {
        u32 key     = 0;
        u64 initval = 1, *valp;

        valp = bpf_map_lookup_elem(&kprobe_map, &key);
        if (!valp) {
                bpf_map_update_elem(&kprobe_map, &key, &initval, BPF_ANY);
                return 0;
        }
        __sync_fetch_and_add(valp, 1);

        return 0;
}

对ELF文件反汇编的结果:

$ llvm-objdump -d bpf_bpfel.o

bpf_bpfel.o:        file format elf64-bpf


Disassembly of section kprobe/sys_execve:

0000000000000000 <kprobe_execve>:
       0:        b7 01 00 00 00 00 00 00        r1 = 0
       1:        63 1a fc ff 00 00 00 00        *(u32 *)(r10 - 4) = r1
       2:        b7 06 00 00 01 00 00 00        r6 = 1
       3:        7b 6a f0 ff 00 00 00 00        *(u64 *)(r10 - 16) = r6
       4:        bf a2 00 00 00 00 00 00        r2 = r10
       5:        07 02 00 00 fc ff ff ff        r2 += -4
       6:        18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00        r1 = 0 ll
       8:        85 00 00 00 01 00 00 00        call 1
       9:        55 00 09 00 00 00 00 00        if r0 != 0 goto +9 <LBB0_2>
      10:        bf a2 00 00 00 00 00 00        r2 = r10
      11:        07 02 00 00 fc ff ff ff        r2 += -4
      12:        bf a3 00 00 00 00 00 00        r3 = r10
      13:        07 03 00 00 f0 ff ff ff        r3 += -16
      14:        18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00        r1 = 0 ll
      16:        b7 04 00 00 00 00 00 00        r4 = 0
      17:        85 00 00 00 02 00 00 00        call 2
      18:        05 00 01 00 00 00 00 00        goto +1 <LBB0_3>

0000000000000098 <LBB0_2>:
      19:        db 60 00 00 00 00 00 00        lock *(u64 *)(r0 + 0) += r6

00000000000000a0 <LBB0_3>:
      20:        b7 00 00 00 00 00 00 00        r0 = 0
      21:        95 00 00 00 00 00 00 00        exit

指令最左边为LSB,最右边为MSB。

  • b7 01 00 00 00 00 00 00
    • opcode是b7,指令类型值为0x07(BPF_ALU64),操作码为0xb0(BPF_MOV),源操作数为0x00(BPF_K),即从imm中取值
    • dst_reg 为R1,imm值为0
    • 总结为 r1 = 0
  • 63 1a fc ff 00 00 00 00
    • opcode是63,指令类型值为0x03(BPF_STX),操作大小为0x00(BPF_W),模式为0x60(BPF_MEM),即将寄存器中的值存储到内存中
    • dst_reg 为R10,src_reg 为 R1,offset值为0xfffc,即-4
    • 总结为*(u32 *) (r10 - 4) = r1
  • 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    • opcode是18,指令类型值为0x00(BPF_LD),操作大小为0x18(BPF_DW),模式为0x00(BPF_IMM),即64位立即数的mov
    • dst_reg 为R1
    • 总结为r1 = 0 ll
  • 85 00 00 00 01 00 00 00
    • opcode是0x85,BPF_JMP | BPF_W | BPF_CALL
    • imm = 0x01
    • call 1
  • 55 00 09 00 00 00 00 00
    • opcode是0x55,BPF_JMP | BPF_B | BPF_JSET(PC += off if dst & src)
    • des_reg和src_reg都为R0,offset为0x09
    • if r0 != 0 goto +9 <LBB0_2>
  • db 60 00 00 00 00 00 00
    • opcode是db,BPF_STX | BPF_DW | BPF_ATOMIC,原子操作使用imm做额外编码,imm为0x00,即BPF_ADD
    • det_reg 为R0,src_reg 为R6,offset 为0
    • lock *(u64 *)(r0 + 0) += r6
  • 95 00 00 00 00 00 00 00
    • opcode是95,BPF_JMP | BPF_K | BPF_EXIT
    • exit