🌐

eBPF - 仮想マシン 編

2022/03/14に公開

1. はじめに

本記事は、eBPFの仮想マシンについてまとめています。

eBPF はカーネルスペースでユーザプログラムを実行するため、安全性のためにサンドボックス化された仮想マシン上でユーザプログラムを実行します。仮想マシン自体は 64 bits の簡易 CPU だと思ってもらってもよく、とはいえ命令数がとても少なく(8bits CPU 時代の CPU よりも命令数は遙かに少ない)、シンプルであるのが特徴です。

以下、関連記事をいくつか書いていますので、必要に応じて参照して下さい。

関連記事

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の例)。

linux/arch/arm64/net/bpf_jit_comp.c
/* 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 アドミックアクセス

例えば、以下のように上記のモードとサイズを組み合わせて目的のロード/ストア処理を実現します。

BPF_MEM | <size> | BPF_LDX
dst_reg = *(size *) (src_reg + off)
BPF_MEM | <size> | BPF_ST
*(size *) (dst_reg + off) = imm32
BPF_MEM | <size> | BPF_STX
*(size *) (dst_reg + off) = src_reg

BPF_ABSBPF_INDはカーネル内のパケットデータのバッファであるsk_buffにアクセスする際にのみ利用が可能です。また、BPF_LD命令はパケットデータからデータをロードする場合に利用する様子です。

BPF_IND | BPF_W | 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

https://github.com/iovisor/ubpf

作成したeBPFのプログラムのソフトウェアライセンスはGPL v2.0にする必要があります。GPLライセンスはプロプラナソフトウェアを扱いたい場合にはとても面倒です。これを回避する手段として、IO VisorはeBPFをユーザランド側で動かせるような仮想マシンを開発しています(どこまで本気かは分かりませんが)。

Discussion