ここでは命令のオペコード(上の「sturct cbpf_insn」「struct ebpf_insn」の「code」の値)について説明します。eBPFはcBPFのオペコードを基本的に受け継いでおり、その上で一部命令が変更、追加されています。
まず、オペコードは下位3bitによって、下記のようなクラスに分けられます。
クラス名 | 値 | 備考 |
---|---|---|
BPF_LD | 0x0 | |
BPF_LDX | 0x1 | |
BPF_ST | 0x2 | |
BPF_STX | 0x3 | |
BPF_ALU | 0x4 | |
BPF_JMP | 0x5 | |
BPF_RET | 0x6 | cBPFのみ |
BPF_MISC | 0x7 | cBPFのみ |
BPF_ALU64 | 0x7 | eBPFのみ |
eBPFではBPF_RETクラスは使用されていません。また、BPF_MISCクラスはBPF_ALU64クラスに変更されています。
クラスが「BPF_ALU」「BPF_JMP」のとき、オペコードは下記のような構成をとります。なおcBPFの命令では、オペコードは16bit幅ですが、実際には下位8bitしか利用されていません。
ここで、sourceの値によりソースオペランドとして何が利用されるかが決まります。
source | 値 | cBPF | eBPF |
---|---|---|---|
BPF_X | 0x00 | Xレジスタ | src_regのレジスタ |
BPF_K | 0x08 | 即値 | 即値 |
ディスティネーションはcBPFの場合はAレジスタ、eBPFの場合はdst_regのレジスタになります。BPF_ALUのとき、operation codeは下記のようになります。
operation code | 値 | 意味 | 備考 |
---|---|---|---|
BPF_ADD | 0x00 | dst += src | |
BPF_SUB | 0x10 | dst -= src | |
BPF_MUL | 0x20 | dst \*= src | |
BPF_DIV | 0x30 | dst /= src | |
BPF_OR | 0x41 | dst|= src | |
BPF_AND | 0x50 | dst &= src | |
BPF_LSH | 0x60 | dst <<= src | |
BPF_RSH | 0x70 | dst >>= src | 論理シフト |
BPF_NEG | 0x80 | dst = -dst | |
BPF_MOD | 0x90 | dst %= src | |
BPF_XOR | 0xa0 | dst ^= src | |
BPF_MOV | 0xb0 | dst = src | eBPFのみ、レジスタ/即値ロード |
BPF_ARSH | 0xc0 | dst >>= src | eBPFのみ、算術シフト |
BPF_END | 0xd0 | dst = convert_endian(dst) | eBPFのみ、下参照 |
operation code、source、instruction classの組み合わせで命令が決定します。例えば、「BPF_ALU | BPF_X | BPF_ADD」はcBPFでは「A += X」、eBPFでは「dst_reg = (u32)dst_reg + (u32) src_reg」という意味になります。
BPF_ENDは、BPF_ALUクラスの中でも特殊な命令であり、下記のようになります。
例えば、「BPF_ALU | BPF_TO_LE | BPF_END」かつimmが32のとき、「dst_reg = (u32) cpu_to_le32(dst_reg)」という意味になります。
BPF_JMPのとき、operation codeは下記のようになります。
operation code | 値 | 分岐条件 | 備考 |
---|---|---|---|
BPF_JA | 0x00 | 無条件 | |
BPF_JEQ | 0x10 | dst == src | |
BPF_JGT | 0x20 | dst > src | |
BPF_JGE | 0x30 | dst >= src | |
BPF_JSET | 0x40 | dst & src | |
BPF_JNE | 0x50 | dst != src | eBPFのみ |
BPF_JSGT | 0x60 | dst > src | eBPFのみ,符号付比較 |
BPF_JSGE | 0x70 | dst >= src | eBPFのみ、符号付比較 |
BPF_CALL | 0x80 | 無条件 | eBPFのみ、関数呼び出し |
BPF_EXIT | 0x90 | 無条件 | eBPFのみ、関数リターン |
BPF_JLT | 0xa0 | dst < src | eBPFのみ |
BPF_JLE | 0xb0 | dst <= src | eBPFのみ |
BPF_JSLT | 0xc0 | dst < src | eBPFのみ、符号付比較 |
BPF_JSLE | 0xd0 | dst <= src | eBPFのみ、符号付比較 |
例として、「BPF_JGE | BPF_K | BPF_JMP」はcBPFでは「pc += (A >= k) ? jt : jf」という意味になります。eBPFでは「pc += (dst_reg >= imm) ? off : 0」という意味になります。
eBPFにはBPF_ALU64クラスがあります。このクラスのときBPF_ALUクラスと同一の命令が利用できますが、計算は64bitで行われます。
例えば、「BPF_ADD | BPF_X | BPF_ALU64」は「dst_reg = dst_reg + src_reg」という意味になります。
「BPF_LD」「BPF_LDX」「BPF_ST」「BPF_STX」のとき、オペコードは下記の構成をとります。
ここで、size、modeは下記の意味になります。
size | 値 | 意味 | 備考 |
---|---|---|---|
BPF_W | 0x00 | 32bit幅 | |
BPF_H | 0x08 | 16bit幅 | |
BPF_B | 0x10 | 8bit幅 | |
BPF_DW | 0x18 | 64bit幅 | eBPFのみ |
mode | 値 | 意味 | 備考 |
---|---|---|---|
BPF_IMM | 0x00 | 即値 | |
BPF_ABS | 0x20 | 絶対参照 | |
BPF_IND | 0x40 | 間接参照 | |
BPF_MEM | 0x60 | メモリアクセス | |
BPF_LEN | 0x80 | 特殊命令(下参照) | cBPFのみ |
BPF_MSH | 0xa0 | 特殊命令(下参照) | cBPFのみ |
BPF_XADD | 0xc0 | アトミック加算 | eBPFのみ |
全ての命令の組み合わせが許可されているわけではありません。cBPFで許可されるのは下記の命令です。
class | size | mode | 意味 |
---|---|---|---|
BPF_LD | BPF_W | BPF_ABS | A = P[k:4] |
BPF_LD | BPF_H | BPF_ABS | A = P[k:2] |
BPF_LD | BPF_B | BPF_ABS | A = P[k:1] |
BPF_LD | BPF_W | BPF_IND | A = P[X+k:4] |
BPF_LD | BPF_H | BPF_IND | A = P[X+k:2] |
BPF_LD | BPF_B | BPF_IND | A = P[X+k:1] |
BPF_LD | BPF_W | BPF_LEN | A = skb->len |
BPF_LD | BPF_W | BPF_IMM | A = k |
BPF_LD | BPF_W | BPF_MEM | A = M[k] |
BPF_LDX | BPF_W | BPF_IMM | X = k |
BPF_LDX | BPF_W | BPF_MEM | X = M[k] |
BPF_LDX | BPF_W | BPF_LEN | X = skb->len |
BPF_LDX | BPF_B | BPF_MSH | X = 4*(P[k:1]&0xf) |
BPF_ST | BPF_W | BPF_ABS | A = k |
BPF_STX | BPF_W | BPF_ABS | X = k |
ここで、「P[]」はcBPFプログラムに渡される引数(通常はパケットデータ)になります。なお、「BPF_MSH」はIPv4ヘッダ内のヘッダ長に効率良くアクセスするための命令です。
cBPFではロード命令(「BPF_LD | BPF_ABS」)において、特別なオフセット値を用いることで、パケットのメタデータなどにアクセスすることが可能です。この機能は「BPF extension」と呼ばれます。下記にBPF extensionに利用されるオフセットと意味を示します。
オフセット | 値 | 意味 |
---|---|---|
SKF_AD_OFF | (-0x1000) | SKF_AD*のベースオフセット |
SKF_AD_PROTOCOL | 0 | skb->protocol |
SKF_AD_PKTTYPE | 4 | skb->pkt_type |
SKF_AD_IFINDEX | 8 | skb->dev->ifindex |
SKF_AD_NLATTR | 12 | bpf_skb_get_nlattr() |
SKF_AD_NLATTR_NEST | 16 | bpf_skb_get_nlattr_nest() |
SKF_AD_MARK | 20 | skb->mark |
SKF_AD_QUEUE | 24 | skb->queue_mapping |
SKF_AD_HATYPE | 28 | skb->dev->type |
SKF_AD_RXHASH | 32 | skb->hash |
SKF_AD_CPU | 36 | bpf_get_raw_cpu_id() |
SKF_AD_ALU_XOR_X | 40 | A ^= X |
SKF_AD_VLAN_TAG | 44 | bpf_skb_vlan_tag_get() |
SKF_AD_VLAN_TAG_PRESENT | 48 | bpf_skb_vlan_tag_present() |
SKF_AD_PAY_OFFSET | 52 | bpf_skb_get_pay_offset() |
SKF_AD_RANDOM | 56 | bpf_user_rnd_u32() |
SKF_AD_VLAN_TPID | 60 | skb->vlan_proto |
ここで、「skb」はカーネル内のパケットデータ構造である「struct sk_buff」を意味します。フィルタリング時にcBPFに渡される引数は、実質的に「skb->data」になります。
注意点として、「skb->data」はパケットがネットワークスタックのどこに位置するかで異なります。例えば、raw socketに対してBPFプログラムをアタッチした場合、BPFプログラムが呼ばれるのはL2スタック処理段階であり、このとき「skb->data」はL2のフレーム先頭になります。一方で、通常のsocketに対してBPFプログラムをアタッチした場合、「skb->data」はL4ヘッダ先頭になります。このときL2のフレーム先頭やL3のヘッダ先頭にアクセスしたい場合、下記の特殊なオフセットが利用できます。
オフセット | 値 | 意味 |
---|---|---|
SKF_NET_OFF | (-0x100000) | L3ヘッダ先頭 |
SKF_LL_OFF | (-0x200000) | L2ヘッダ先頭 |
eBPFでは、汎用的なロードストア命令がサポートされています。下記にeBPFで使用できる命令を示します。
class | size | mode | 意味 |
---|---|---|---|
BPF_LD | BPF_B/H/W/DW | BPF_ABS | 特殊命令(下参照) |
BPF_LD | BPF_B/H/W/DW | BPF_IND | 特殊命令(下参照) |
BPF_LD | BPF_DW | BPF_IMM | dst_reg = imm64 |
BPF_LDX | BPF_B/H/W/DW | BPF_MEM | dst_reg = *(size *)(src_reg + off) |
BPF_ST | BPF_B/H/W/DW | BPF_MEM | *(size *)(dst_reg + off) = imm |
BPF_STX | BPF_B/H/W/DW | BPF_MEM | *(size *)(dst_reg + off) = src_reg |
BPF_STX | BPF_W | BPF_XADD | lock xadd *(u32*)(dst_reg + off16) += src_reg |
BPF_STX | BPF_DW | BPF_XADD | lock xadd *(u64*)(dst_reg + off16) += src_reg |
「BPF_LD | BPF_DW |BPF_IMM」では64bit即値を表現するために、2つの連続した「struct bpf_insn」を利用します。最初の「bpf_insn.imm」で下位32bit、次の「bpf_insn.imm」で上位32bitを表現します。これはeBPFで唯一の16バイト命令になります。32bit即値のロードには「BPF_ALU | BPF_K | BPF_MOV」を利用します。
もともとcBPFでは「BPF_LD | BPF_ABS」や「BPF_LD | BPF_IND」の一命令でパケットデータをロードしていました。このときエンディアンはホストオーダーに自動で変換されます。
一方eBPFをネットワーク関連のイベントにアタッチした場合、コンテキストとして渡される引数はsk_buffです。eBPFでは汎用的なメモリアクセス命令(「BPF_LDX | BPF_MEM」)が存在するため、理論的にはこの命令を利用してsk_buff内のパケットデータにアクセスできます。
しかし、eBPF導入当初は直接sk_buff内のパケットデータに直接アクセスすることはできませんでした。この理由の一つは、検証器がメモリアクセスが正しい範囲内に収まっているかを静的に検証することが簡単ではないためだと思われます。
eBPFでも「BPF_LD | BPF_ABS」や「BPF_LD | BPF_IND」を利用してエンディアンがホストオーダーに変換済みのパケットデータをロードできます。
ただし、これらは他の命令と比べて特殊な命令となっており、命令実行前にR6にコンテキスト(sk_buff)の値を入れておく必要があります。結果はR0に格納されます。
例えば、「BPF_IND | BPF_W | BPF_LD」は、「R0 = ntohl(*(u32 *) (((struct sk_buff *) R6)->data + src_reg + imm32))」という意味になります。この命令は内部では関数呼び出し(「bpf_skb_load_helper*()」)として実行されます(従って、命令実行前にR1〜R5のレジスタは必要に応じて退避する必要があります)。この関数の中では動的にオフセットの境界チェックを行っており、仮に境界外アクセスをしようとした場合はその時点でプログラムの実行が終了し、0が戻り値として返されます。
今では、パフォーマンスを向上させるためには、XDPなどではBPFプログラムから直接パケットデータにアクセスする「Direct Packet Access」が利用できます。Direct Packet Accessについては次回の「検証器の説明」の際に詳しく触れます。
なおコンテキストがsk_buff以外の場合、BPF_ABSやBPF_INDを使った命令は利用できません(検証器が実行を許可しません)。
cBPFにはBPF_RETクラスが存在します。BPF_RETクラスではオペコードは下記の構成をとります。
source | 値 | 意味 |
---|---|---|
BPF_K | 0x08 | 即値 |
BPF_A | 0x18 | Aレジスタ |
sourceによってcBPFプログラムが関数終了時に返す値を指定します。Xレジスタの値を返すことはできません。
eBPFではリターンには「BPF_JMP | BPF_EXIT」を利用します。このとき、戻り値はR0に格納します。
cBPFにはBPF_MISCクラスが存在します。BPF_MISCクラスではオペコードは下記の構成をとります。
operation code | 値 | 意味 |
---|---|---|
BPF_TAX | 0x00 | X = A |
BPF_TXA | 0x80 | A = X |
eBPFではBPF_MISCクラスは利用されていません。レジスタ間の移動には「BPF_ALU | BPF_X | BPF_MOV」を利用します。
今回は、BPFの基礎としてBPFのアーキテクチャについて説明しました。次回も引き続き基礎として、BPFプログラムの作成方法、BPFの検証器およびJITコンパイル機能などについて説明します。
東京大学 大学院 情報理工学系研究科 博士課程
オペレーティングシステムや仮想化技術の研究に従事。
Copyright © ITmedia, Inc. All Rights Reserved.