連載
» 2018年12月10日 05時00分 公開

BPFのアーキテクチャ、命令セット、cBPFとeBPFの違いBerkeley Packet Filter(BPF)入門(2)(2/2 ページ)

[味曽野雅史,OSSセキュリティ技術の会]
前のページへ 1|2       

命令セットの詳細

 ここでは命令のオペコード(上の「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クラス

 クラスが「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クラスの中でも特殊な命令であり、下記のようになります。

  • sourceの値が0(BPF_TO_LE)のとき、リトルエンディアンへ変換
  • sourceの値が1(BPF_TO_BE)のとき、ビッグエンディアンへ変換
  • immの値でビット幅を指定(16、32、64のいずれか)

 例えば、「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」という意味になります。

BPF_ALU64クラス

 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 クラス

 「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」を利用します。

eBPFにおけるBPF_ABS、BPF_IND

 もともと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を使った命令は利用できません(検証器が実行を許可しません)。

BPF_RETクラス

 cBPFにはBPF_RETクラスが存在します。BPF_RETクラスではオペコードは下記の構成をとります。

source 意味
BPF_K 0x08 即値
BPF_A 0x18 Aレジスタ

 sourceによってcBPFプログラムが関数終了時に返す値を指定します。Xレジスタの値を返すことはできません。

 eBPFではリターンには「BPF_JMP | BPF_EXIT」を利用します。このとき、戻り値はR0に格納します。

BPF_MISCクラス

 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の検証器、JITコンパイル機能

 今回は、BPFの基礎としてBPFのアーキテクチャについて説明しました。次回も引き続き基礎として、BPFプログラムの作成方法、BPFの検証器およびJITコンパイル機能などについて説明します。

筆者紹介

味曽野 雅史(みその まさのり)

東京大学 大学院 情報理工学系研究科 博士課程

オペレーティングシステムや仮想化技術の研究に従事。


前のページへ 1|2       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。