BPFのアーキテクチャ、命令セット、cBPFとeBPFの違い:Berkeley Packet Filter(BPF)入門(2)(1/2 ページ)
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、Linuxで用いられるBPFのアーキテクチャなどを説明する。
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載「Berkeley Packet Filter(BPF)入門」。初回は、BPFの歴史や概要について解説しました。今回はBPFの基礎として、Linuxで用いられるBPFのアーキテクチャなどを説明します。
現在Linuxで主として用いられている「eBPF(extended BPF)」はオリジナルの「cBPF(classic BPF)」とは異なり、互換性がありません。しかし、LinuxにeBPFが導入された後も、LinuxからcBPFが完全になくなったわけではありません。cBPFを利用したパケットフィルタリングも可能ですし(もちろんeBPFでもできます)、システムコールフィルター(seccomp)は依然としてeBPFではなくcBPFを入力として受け取ります。
ただし、内部的にはcBPFのプログラムはeBPFのプログラムに変換されて実行されています。
以下では、cBPFとeBPF双方のアーキテクチャについて簡単にまとめます。これは今後の連載のレファレンスを意識したものであり、BPFの利用に当たって覚えておく必要はありません。必要に応じて適宜参照してください。また正式なドキュメンテーションは「Linux Socket Filtering aka Berkeley Packet Filter(BPF)」にあります。
cBPFのアーキテクチャ
cBPFの命令セットは64bit固定長です。フォーマットをC言語で表現すると、下記のようになります。
struct cbpf_insn { u16 code; /* オペコード */ u8 jt; /* 条件が真のときの分岐先 */ u8 jf; /* 条件が偽のときの分岐先 */ u32 k; /* 汎用フィールド */ };
ジャンプ先のオフセット(jt、jf)はunsignedなので、cBPFでは負の方向へのジャンプができません。つまり、ループ処理はできません。この仕様とプログラムサイズを制限することで、一定時間内でのプログラム実行終了が保証されます。
仮想マシンは下記のレジスタとメモリ領域を持ちます。レジスタは32bit幅です。
レジスタ名 | 説明 |
---|---|
A | アキュムレータレジスタ |
X | インデックスレジスタ |
M[] | メモリ領域、32bit幅、サイズ16 |
cBPFには下記のような命令があります。
- ロード命令
- ld、ldx
- AレジスタまたはXレジスタにパケットのデータもしくはメモリ領域のデータをロード
- ストア命令
- st、stx
- メモリ領域にAレジスタまたはXジレスタの値をストア
- ジャンプ命令
- jmp、ja、jeq、jneq、jne、jlt、jle、jgt、jge、jset
- 算術命令
- add、sub、mul、div、mod、neg、and、or、xor、lsh、rsh
- リターン命令
- ret
- Aレジスタの値もしくは即値をリターン
パケットデータに対するストア命令はありません。
eBPFのアーキテクチャ
eBPFの命令セットは64bit固定長です。フォーマットをC言語で表現すると、下記のようになります。
struct ebpf_insn { u8 code; /* オペコード */ u8 dst_reg:4; /* ディスティネーションレジスタ */ u8 src_reg:4; /* ソースレジスタ */ s16 off; /* オフセット */ s32 imm; /* 即値 */ };
オフセットや即値はsigned型になっています。従って、cBPFとは異なり、負方向へのジャンプが可能です。プログラムの一定時間以内の動作終了は検証器が検証します。また、「dst_reg」「src_reg」フィールドで使用するレジスタが指定できるようになっています。
一方で、cBPFにあった「jt」「jf」フィールドはeBPFには存在しません。eBPFのジャンプ命令は、指定した番地へのジャンプあるいはフォールスルーのどちらかになります。
仮想マシンは下記のレジスタを持ちます。レジスタは64bit幅です。
レジスタ名 | 説明 |
---|---|
R0 | 汎用レジスタ(戻り値を格納) |
R1〜R5 | 汎用レジスタ(引数レジスタ) |
R6〜R9 | 汎用レジスタ |
R10 | フレームポインタ(読み出し専用) |
x86_64やAArch64などのアーキテクチャにおいて、eBPFのレジスタは実際のCPUのレジスタに1対1で対応付けられるようになっています。eBPFの呼び出し規約は64bitカーネルで利用されるものと直接対応します。
cBPFとeBPFの違い
cBPFとeBPFには、下記のような違いがあります。
cBPF | eBPF | |
---|---|---|
レジスタ数 | 2 | 10 |
レジスタ幅(bit) | 32 | 64 |
スタックサイズ(Byte) | 16 | 512 |
スタックアクセスサイズ(Byte) | 4 | 1、2、4、8 |
パケットアクセスサイズ(Byte) | 1、2、4 | 1、2、4、8 |
外部関数呼び出し | × | ◯ |
負方向への分岐 | × | ◯ |
アトミック加算命令 | × | ◯ |
フレームポインタ
cBPFに存在したメモリ領域の代わりに、eBPFではR10のフレームポインタを利用してプログラム用のスタック(512B)にアクセスできます。
命令数の増加
eBPFは一部を除きcBPFと同等の命令をサポートする他、以下のような命令が追加されています。
- 外部関数呼び出し命令
- 事前に登録済みのカーネル内関数を呼び出す(後述)
- アトミック加算命令
- 主にbpf mapのデータ構造内のデータを更新する際に利用する
- エンディアン変換命令
- 64bit幅命令
- 64bit幅でのメモリの読み書き、および64bit即値のロード
またcBPFでは多くの命令がAレジスタに対するものであったのに対し、eBPFでは「src_reg」「dst_reg」フィールドにより使用するレジスタを柔軟に選択できるようになっています。
外部関数呼び出し
eBPFでは事前に登録済みのカーネル内関数をBPFプログラムから呼び出すことが可能です。このとき、呼び出し元はR1〜R5に適切に引数を設定して関数を呼び出します。戻り値はR0に格納されます。
必要であればR0-R5の値は呼び出し前に退避する必要があります。R6〜R9のレジスタは、関数側が退避する規約になっているため、呼び出し元が退避する必要はありません。引数が6以上の関数呼び出しはサポートされていません。
プログラムタイプ
eBPFは現在ネットワーク用途のみならず、さまざまな用途で利用されています。利用場面に応じて、BPFプログラムが可能な操作(検証器が検証する内容)や呼び出し可能な外部関数は異なります。
カーネルではBPFの種類を「BPF_PROG_TYPE」で識別しています。カーネル4.18時点で以下のタイプが存在します。
enum bpf_prog_type { BPF_PROG_TYPE_UNSPEC, BPF_PROG_TYPE_SOCKET_FILTER, BPF_PROG_TYPE_KPROBE, BPF_PROG_TYPE_SCHED_CLS, BPF_PROG_TYPE_SCHED_ACT, BPF_PROG_TYPE_TRACEPOINT, BPF_PROG_TYPE_XDP, BPF_PROG_TYPE_PERF_EVENT, BPF_PROG_TYPE_CGROUP_SKB, BPF_PROG_TYPE_CGROUP_SOCK, BPF_PROG_TYPE_LWT_IN, BPF_PROG_TYPE_LWT_OUT, BPF_PROG_TYPE_LWT_XMIT, BPF_PROG_TYPE_SOCK_OPS, BPF_PROG_TYPE_SK_SKB, BPF_PROG_TYPE_CGROUP_DEVICE, BPF_PROG_TYPE_SK_MSG, BPF_PROG_TYPE_RAW_TRACEPOINT, BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_PROG_TYPE_LWT_SEG6LOCAL, BPF_PROG_TYPE_LIRC_MODE2, }
eBPF map
eBPFでは「eBPF map」と呼ばれるデータ構造が利用可能です。eBPF mapは、BPFプログラムからは外部関数呼び出し機能を利用して、ユーザーアプリケーションからはシステムコールを利用して、それぞれアクセス可能です。
eBPF mapを利用することで、eBPFプログラムの状態を管理できます。eBPF mapの具体的な説明は、後の連載で、実際に利用する場面で行います。
コンテキスト
cBPFでは引数は非明示的な形で与えられ、ロード命令を利用してその中身をレジスタに読み込むことが可能です。
一方eBPFでは、最初にeBPFプログラムが実行されるときR1レジスタに引数が渡されます。これをeBPFでは「コンテキスト」と呼んでいます。非明示的な引数は存在しません。また複数のコンテキストがBPFプログラムに渡されることはありません。
コンテキストはBPF_PROG_TYPEごとに異なります。例えば、ネットワーキング関連の場合コンテキストにはカーネル内のパケットのデータ構造である「struct sk_buff」が渡されます。
Tail Call
「Tail Call」は他のBPFプログラムへの遷移を行う機能です。遷移後に遷移元に戻ることはありません。遷移先のBPFプログラムとはスタックフレームを共有します。
一定時間での終了を保証するために最大のTail Callの回数は32に制限されています。
他のBPF関数の呼び出し
Linux 4.16およびLLVM 6.0から、他のBPF関数をBPFプログラムから呼び出せるようになりました。呼び出し方法は外部関数呼び出しに準拠します。それ以前では、BPFプログラムから他のBPF関数を呼び出したい場合は、全てインラインで展開する必要がありました。
BPF関数を呼び出す場合、最大の呼び出しネスト回数は8に制限されています。引数として呼び出し元のframe pointerを渡すことは可能ですが、逆は不可能です。
この機能はTail Callと合わせて利用することはできません。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- 2018年上半期に話題になったSpectreとその変異、Linuxカーネルでの対応まとめ
連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説していく。2018年の上半期は、「Meltdown」「Spectre」とその変異(Variant)の脆弱性に悩まされた。今回はいつもとは異なり、上半期のまとめも兼ねて、Meltdown/Spectreの各変異をバージョンを追いかけながら整理する。 - 2017年版Linuxカーネル開発レポート公開――支援している企業トップ10とは?
The Linux Foundationは2017年版Linuxカーネル開発レポートを公開した。Linuxカーネル4.8から4.13までの開発に焦点を当て、カーネル開発に携わった開発者や変更数などについて言及した。 - Linuxカーネルのソースコードを読んで、システムコールを探る
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。前回まで、printf()内の中身をさまざまな方法で探り、write()やint $0x80の呼び出しまでたどり着いた。今回は、さらにその先にあるLinuxカーネル側のシステムコールを見ていく。