金庆辉,系统工程师,对系统性能分析领域有强烈的兴趣。
版权说明:本文档翻译自官方文档,额外加入了自己的一些理解。
目的:编译成功的eBPF程序,加载时偶尔会过不了内核BPF verifier,冒出一堆汇编语句。理解eBPF指令集,可以帮助我们调试这类问题。
本文档是eBPF指令集规范,版本 1.0
eBPF有10个通用寄存器和一个只读的栈帧寄存器,他们都是64-bit宽度。
eBPF的寄存器使用规范为:
R0: 保存函数返回值和eBPF程序退出值。
R1 - R5: 用于函数调用参数。
R6 - R9: callee函数负责进入时保存这几个寄存器到栈中,函数退出前再恢复寄存器原有值。(callee saved registers that function calls will preserve)
R10: 只读的栈帧寄存器,用于访问栈。
R0 - R5是临时寄存器,eBPF程序如果希望在函数调用后寄存器值不变,需要自己保存和恢复寄存器。(R0 - R5 are scratch registers and eBPF programs needs to spill/fill them if necessary across calls.)
译者注:
Scratch register / temporary register:顾名思义,用于保存临时值或者中间值。
Caller 和 Callee: A函数中调用B函数,那么 A是Caller,B是Callee。
Caller saved registers 和 Callee saved registers
Caller saved registers(AKA volatile registers, or call-clobbered) | Callee saved registers(AKA non-volatile registers, or call-preserved) |
---|---|
Caller函数负责保存和恢复寄存器(也可以不保存和恢复) | Callee函数负责保存和恢复寄存器,这样寄存器的值在子函数调用后不会改变 |
更多资料
eBPF有两种编码模式:
基础编码,64bit固定长度编码。
宽指令编码,在基础编码后增加了一个64bit的立即数(imm64)。这样指令为128bit。
基础编码格式,每一列约定为field
:
32 bits (MSB,最高bit位) | 16 bits | 4 bits | 4 bits | 8 bits (LSB,最低比特位) |
---|---|---|---|---|
imm32(立即数) | off16(偏移) | src_reg(源寄存器) | dst_reg(目的寄存器) | opcode(操作码) |
注意:绝大多数指令不会使用所有的field
,不使用的field
被置0。
宽指令编码目前只有64-bit立即数指令使用。
基础编码中的field
的opcode
,一共8bit,其中最低位3bit用来保存指令类型:
class | value | description | reference |
---|---|---|---|
BPF_LD | 0x00 | 只能用于宽指令,从imm64 中加载数据到寄存器 | Load 和 store指令 |
BPF_LDX | 0x01 | 从内存中加载数据到dst_reg | Load 和 store指令 |
BPF_ST | 0x02 | 把imm32 数据保存到内存中 | Load 和 store指令 |
BPF_STX | 0x03 | 把src_reg 寄存器数据保存到内存 | Load 和 store指令 |
BPF_ALU | 0x04 | 32bit算术运算 | 算术和跳转指令 |
BPF_JMP | 0x05 | 64bit跳转操作 | 算术和跳转指令 |
BPF_JMP32 | 0x06 | 32bit跳转操作 | 算术和跳转指令 |
BPF_ALU64 | 0x07 | 64bit算术运算 | 算术和跳转指令 |
(BPF_ALU, BPF_ALU64, BPF_JMP和BPF_JMP32)称为算术和跳转指令
。8bit的opcode
被分为3个部分:
4 bits (MSB,最高bit位) | 1 bit | 3 bits (LSB,最低bit位) |
---|---|---|
operation code | source | 指令类型(BPF_ALU, BPF_ALU64, BPF_JMP或BPF_JMP32) |
第4个bit(source
)含义:
source | value | description |
---|---|---|
BPF_K | 0x00 | 使用32-bitimm32 作为源操作数 |
BPF_X | 0x08 | 使用源寄存器(src_reg )作为源操作数 |
4个bit的operation code
用来存储具体指令操作码。
BPF_ALU操作数为32bit,BPF_ALU64操作数为64bit。4个bit的operation code
编码了如下操作:
operation code | value | description |
---|---|---|
BPF_ADD | 0x00 | dst += src |
BPF_SUB | 0x10 | dst -= src |
BPF_MUL | 0x20 | dst *= src |
BPF_DIV | 0x30 | dst /= src |
BPF_OR | 0x40 | dst |= src |
BPF_AND | 0x50 | dst &= src |
BPF_LSH | 0x60 | dst <<= src |
BPF_RSH | 0x70 | dst >>= src |
BPF_NEG | 0x80 | dst = ~src |
BPF_MOD | 0x90 | dst %= src |
BPF_XOR | 0xa0 | dst ^= src |
BPF_MOV | 0xb0 | dst = src |
BPF_ARSH | 0xc0 | 算术右移操作。对于负数,右移会在左边最高位补上右移次数个1,对于正数则补0 |
BPF_END | 0xd0 | 字节的swap操作 |
译者注:
上表中dst一定是指目的寄存器,不支持内存地址。
上表中src可能是源寄存器,也可能是imm32
,根据source位(BPF_K或者BPF_X)来区分。
eBPF寄存器都是64bit,根据操作数类型,可以使用全部64bit,也可以只使用其中32bit。
一些例子:
BPF_ADD | BPF_X | BPF_ALU
:
dst_reg = (u32) dst_reg + (u32) src_reg;
BPF_XOR | BPF_K | BPF_ALU64
:
dst_reg = dst_reg + src_reg
BPF_XOR | BPF_K | BPF_ALU
:
dst_reg = (u32) dst_reg ^ (u32) imm32
BPF_XOR | BPF_K | BPF_ALU64
:
dst_reg = dst_reg ^ imm32
字节swap指令,属于BPF_ALU分类,操作码为BPF_END。
字节swap指令操作数只有dst_reg
,不操作src_reg
和imm32
。
opcode
中的source
位含义现在更改为:
source | value | description |
---|---|---|
BPF_TO_LE | 0x00 | 主机字节序到小端字节序 |
BPF_TO_BE | 0x08 | 主字节序序到大端字节序 |
基础编码格式中的imm32
,此时编码了swap操作的位宽。支持:16,32和64bit。
例子:
BPF_ALU | BPF_TO_LE | BPF_END
,并且imm32
= 16:
dst_reg = htole16(dst_reg)
BPF_ALU | BPF_TO_LE | BPF_END
,并且imm32
= 64:
dst_reg = htole64(dst_reg)
操作数为寄存器,BPF_JMP32使用32bit,BPF_JMP使用64bit,其它行为都一样。operation code
含义如下:
operation code | value | description | notes |
---|---|---|---|
BPF_JA | 0x00 | PC += off | 仅用在BPF_JMP中 |
BPF_JEQ | 0x10 | PC += off if dst == src | |
BPF_JGT | 0x20 | PC += off if dst > src | unsigned |
BPF_JGE | 0x30 | PC += off if dst >= src | unsigned |
BPF_JSET | 0x40 | PC += off if dst & src | |
BPF_JNE | 0x50 | PC += off if dst != src | |
BPF_JSGT | 0x60 | PC += off if dst > src | signed |
BPF_JSGE | 0x70 | PC += off if dst >= src | signed |
BPF_CALL | 0x80 | 函数调用 | |
BPF_EXIT | 0x90 | 函数或者程序返回 | 仅用在BPF_JMP分类中 |
BPF_JLT | 0xa0 | PC += off if dst < src | unsigned |
BPF_JLE | 0xb0 | PC += off if dst <= src | unsigned |
BPF_JSLT | 0xc0 | PC += off if dst < src | signed |
BPF_JSLE | 0xd0 | PC += off if dst <= src | signed |
eBPF程序在调用BPF_EXIT前,需要把返回值保存在R0寄存器中。
译者注: 上表中的术语:
PC:程序计数器。
off: 基础编码格式中的off16
。
src,dst: 都是指的寄存器的值。
BPF_LD, BPF_LDX, BPF_ST和BPF_STX指令类型中,8bit的opcode
含义为:
3 bits (MSB) | 2 bit | 3 bits (LSB) |
---|---|---|
mode | size | 指令类型(BPF_LD, BPF_LDX, BPF_ST或BPF_STX) |
mode
含义是:
mode | value | description | reference |
---|---|---|---|
BPF_IMM | 0x00 | 64bit立即数指令 | 64bit立即数指令 |
BPF_ABS | 0x20 | 经典BPF 数据包访问(直接) | 经典BPF数据包访问指令 |
BPF_IND | 0x40 | 经典BPF 数据包访问(间接) | 经典BPF数据包访问指令 |
BPF_MEM | 0x60 | 标准load和store操作 | 标准load和store指令 |
BPF_ATOMIC | 0xc0 | 原子操作 | 原子操作 |
size
含义:
size | value | description |
---|---|---|
BPF_W | 0x00 | 字长(4字节) |
BPF_H | 0x08 | 半字长(2字节) |
BPF_B | 0x010 | 字节(1字节) |
BPF_DW | 0x18 | 双字长(8字节) |
BPF_MEM
代表了标准load和store指令,这些指令用于寄存器和内存之间传递数据。
BPF_MEM |
:
*(size *) (dst_reg + off16) = src_reg
BPF_MEM |
:
*(size *) (dst_reg + off16) = imm32
BPF_MEM |
:
dst_reg = *(size *) (src_reg + off16)
size
可选值:BPF_B, BPF_H, BPF_W, or BPF_DW
原子操作,指的的是对内存的操作,不会被其他eBPF程序中途扰乱。
eBPF所有的原子操作由BPF_ATOMIC
指定,例如:
BPF_ATOMIC | BPF_W | BPF_STX
:32-bit原子操作。
BPF_ATOMIC | BPF_DW | BPF_STX
:64-bit原子操作。
8-bit和16-bit原子操作不支持。
基本编码格式的imm32
用来编码真正的原子操作, 以下是简单原子指令
:
imm32 | value | description |
---|---|---|
BPF_ADD | 0x00 | 原子加 |
BPF_OR | 0x40 | 原子或 |
BPF_AND | 0x50 | 原子与 |
BPF_XOR | 0xa0 | 原子异或 |
BPF_ATOMIC | BPF_W | BPF_STX
, imm32 = BPF_ADD
,含义:
*(u32 *)(dst_reg + off16) += src_reg
BPF_ATOMIC | BPF_DW | BPF_STX
, imm32 = BPF_ADD
, 含义:
*(u64 *)(dst_reg + off16) += src_reg
除了以上比较简单的原子操作,还有2个复杂原子指令
:
imm32 | value | description |
---|---|---|
BPF_XCHG | 0xe0|BPF_FETCH | 原子交换,交换src_reg 和(dst_reg + off16)指向内存 的值 |
BPF_CMPXCHG | 0xf0|BPF_FETCH | 原子CAS.if (*(uXX*)(dst_reg + off16) == R0) { *(uXX*)(dst_reg + off16) = (src_reg) }; 无论是否交换成功,R0都会保存(dst_reg + off16)指向内存 的被修改前的原始值。如果是32bit操作数,那么用会0补齐高位后再保存到R0。 |
BPF_FETCH
:
imm32 | value | description |
---|---|---|
BPF_FETCH | 0x01 | 代表需要返回旧值 |
对于简单原子指令
是可选的,如果设置了,src_reg
将保存(dst_reg + off16)
指向的内存中被修改前的原始值。
对于复杂原子指令
是必选的。
mode
为BPF_IMM
的指令,用于eBPF宽指令,有额外的一个imm64
值。
目前只有一条指令:BPF_LD | BPF_DW | BPF_IMM
,含义为:dst_reg = imm64
eBPF之前为了兼容经典BPF
,引入了一些访问数据包的指令。现在已经废弃不再使用。