eBPF - 仮想マシン 編
本記事は、eBPFの仮想マシンについてまとめています。
eBPF はカーネルスペースでユーザプログラムを実行するため、安全性のためにサンドボックス化された仮想マシン上でユーザプログラムを実行します。仮想マシン自体は 64 bits の簡易 CPU だと思ってもらってもよく、とはいえ命令数がとても少なく(8bits CPU 時代の CPU よりも命令数は遙かに少ない)、シンプルであるのが特徴です。
以下、関連記事をいくつか書いていますので、必要に応じて参照して下さい。
- eBPF - 入門概要 編
- eBPF - 仮想マシン 編(本記事)
- eBPF - BCCチュートリアル 編
- eBPF - bpftraceチュートリアル 編
- eBPF - XDP概要 編
2. レジスタ
64bit x 11個の汎用レジスタ(R0 - R10)が用意されています。
レジスタ名 | アクセス属性 | 用途 |
---|---|---|
R0 | RW | 汎用レジスタ(関数の戻り値を格納) |
R1 - R5 | RW | 汎用レジスタ(関数コールの引数を格納) |
R6 - R9 | RW | 汎用レジスタ |
R10 | RO | スタックポインタ |
R1 - R5レジスタ
eBPFのプログラムからカーネル内の特定の関数を呼び出すことが可能ですが、この時の関数コールの引数として利用されます。なお、関数の引数の個数の上限はこの制約から5つです。
uint64_t fn(uint64_t R1, uint64_t R2, uint64_t R3, uint64_t R4, uint64_t R5)
R0レジスタ
eBPFのプログラムおよびカーネル関数のリターン値として利用されます。
R10レジスタ
スタックポインタとして利用されます(Read Only)。
スタックサイズ
512バイト固定で上述の通りR10
レジスタを利用します。
CPUアーキテクチャ毎のレジスタ割り当て
仮想マシンのレジスタは、JIT動作のためにホストCPUの実レジスタに割り当てられて利用されています(↓arm64の例)。
/* Map BPF registers to A64 registers */
static const int bpf2a64[] = {
/* return value from in-kernel function, and exit value from eBPF */
[BPF_REG_0] = A64_R(7),
/* arguments from eBPF program to in-kernel function */
[BPF_REG_1] = A64_R(0),
[BPF_REG_2] = A64_R(1),
[BPF_REG_3] = A64_R(2),
[BPF_REG_4] = A64_R(3),
[BPF_REG_5] = A64_R(4),
/* callee saved registers that in-kernel function will preserve */
[BPF_REG_6] = A64_R(19),
[BPF_REG_7] = A64_R(20),
[BPF_REG_8] = A64_R(21),
[BPF_REG_9] = A64_R(22),
/* read-only frame pointer to access stack */
[BPF_REG_FP] = A64_R(25),
/* temporary registers for BPF JIT */
[TMP_REG_1] = A64_R(10),
[TMP_REG_2] = A64_R(11),
[TMP_REG_3] = A64_R(12),
/* tail_call_cnt */
[TCALL_CNT] = A64_R(26),
/* temporary register for blinding constants */
[BPF_REG_AX] = A64_R(9),
};
3. 命令セット
各種算術演算、ストア、ロード、ジャンプ命令以外にも、カーネル関数のコール命令やアトミック命令、カーネル内のネットワークパケットデータへのアクセス、エンディアン変換などの命令が用意されています。
命令のフォーマット
eBPFの命令セットは1命令あたり64bit固定長でとてもシンプルです。
63-32 bits (MSB) | 31-16 bits | 15-12 bits | 11-8 bits | 7-0 bits (LSB) |
---|---|---|---|---|
イミディエート | オフセット | ソースレジスタ | デスティネーションレジスタ | オペコード |
struct bpf_insn {
__u8 code; /* opcode */
__u8 dst_reg:4; /* dest register */
__u8 src_reg:4; /* source register */
__s16 off; /* signed offset */
__s32 imm; /* signed immediate constant */
};
命令クラス
eBPFの命令は8ビットのオペコードの内のLSB 3bitを利用して、8グループに分けられます。各グループでその次の命令デコードの方法が異なってきます。
クラス | 値 | 説明 |
---|---|---|
BPF_LD | 0 | ロード命令(特殊なユースケース場合にのみ使用) |
BPF_LDX | 1 | ロード命令(レジスタへのロード) |
BPF_ST | 2 | ストア命令(即値をストア) |
BPF_STX | 3 | ストア命令(レジスタからストア) |
BPF_ALU | 4 | 32 bit算術命令 |
BPF_JMP | 5 | 64 bitジャンプ命令 |
BPF_JMP32 | 6 | 32 bitジャンプ命令 |
BPF_ALU64 | 7 | 64 bit算術命令 |
命令一覧
ロード/ストア命令
後述のロード/ストア命令は複数のデータサイズとモード(主にアドレッシングモード)をサポートしています。オペコードの8bitを以下のルールでデコードすることで判定します。
7-5 bits (MSB) | 4-3 bits | 2-0 bits (LSB) |
---|---|---|
アドレッシングモード | サイズ | 命令クラス |
サイズ
サイズ | 値 | 説明 |
---|---|---|
BPF_W | 0x00 | 4バイト |
BPF_H | 0x08 | 2バイト |
BPF_B | 0x10 | 1バイト |
BPF_DW | 0x18 | 8バイト |
モード
モード | 値 | 説明 |
---|---|---|
BPF_IMM | 0x00 | 即値 |
BPF_ABS | 0x20 | 絶対参照 |
BPF_IND | 0x40 | 間接参照 |
BPF_MEM | 0x60 | メモリアクセス |
BPF_ATOMIC | 0xc0 | アドミックアクセス |
例えば、以下のように上記のモードとサイズを組み合わせて目的のロード/ストア処理を実現します。
dst_reg = *(size *) (src_reg + off)
*(size *) (dst_reg + off) = imm32
*(size *) (dst_reg + off) = src_reg
BPF_ABS
とBPF_IND
はカーネル内のパケットデータのバッファであるsk_buff
にアクセスする際にのみ利用が可能です。また、BPF_LD
命令はパケットデータからデータをロードする場合に利用する様子です。
R0 = ntohl(*(u32 *) (((struct sk_buff *) R6)->data + src_reg + imm32))
算術命令
ALUは32bitモード(BPF_ALU)と64bitモード(BPF_ALU64)を持っています。
ニーモニック | 説明 |
---|---|
BPF_ADD | dst += src |
BPF_SUB | dst -= src |
BPF_MUL | dst *= src |
BPF_DIV | dst /= src |
BPF_OR | dst |
BPF_AND | dst &= src |
BPF_LSH | dst <<= src |
BPF_RSH | dst >>= src(論理シフト) |
BPF_NEG | dst = ~src |
BPF_MOD | dst %= src |
BPF_XOR | dst ^= src |
BPF_MOV | dst = src |
BPF_ARSH | dst >>= src(算術シフト、つまり符号ビットを維持した状態で右シフト) |
BPF_END | dst = エンディアン変換(src) |
ジャンプ命令
ニーモニック | 説明 | ジャンプ条件 |
---|---|---|
BPF_JA | PC += offset | 常時 |
BPF_JEQ | PC += offset | dst == src |
BPF_JGT | PC += offset(unsigned) | dst > src |
BPF_JGE | PC += offset(unsigned) | dst >= src |
BPF_JSET | PC += offset | dst & src > 0 |
BPF_JNE | PC += offset | dst != src |
BPF_JSGT | PC += offset(signed) | dst > src |
BPF_JSGE | PC += offset(signed) | dst >= src |
BPF_CALL | 関数コール | 常時 |
BPF_EXIT | 関数/プログラムの終了 | 常時 |
BPF_JLT | PC += offset(unsigned) | dst < src |
BPF_JLE | PC += offset(unsigned) | dst <= src |
BPF_JSLT | PC += offset(signed) | dst < src |
BPF_JSLE | PC += offset(signed) | dst <= src |
4. その他
upbf
作成したeBPFのプログラムのソフトウェアライセンスはGPL v2.0
にする必要があります。GPLライセンスはプロプラナソフトウェアを扱いたい場合にはとても面倒です。これを回避する手段として、IO Visor
はeBPFをユーザランド側で動かせるような仮想マシンを開発しています(どこまで本気かは分かりませんが)。
Discussion