BPFプログラムの作成方法、BPFの検証器、JITコンパイル機能:Berkeley Packet Filter(BPF)入門(3)(1/3 ページ)
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、BPFプログラムの作成方法、BPFの検証器、JITコンパイル機能について解説します。
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載「Berkeley Packet Filter(BPF)入門」。初回は、BPFの歴史や概要について解説し、前回はBPFの基礎として、Linuxで用いられるBPFのアーキテクチャなどを説明しました。
今回は、BPFプログラムの作成方法、BPFの検証器、JITコンパイル機能について解説します。
BPFプログラムの作成方法
BPFのプログラムを作成するには主に以下の方法があります。
- BPF命令列を直接定義する
- BPFアセンブラを利用する
- tcpdump(libpcap)でフィルター式をcBPFにコンパイルする
- Clang(LLVM)でCのプログラムをeBPFにコンパイルする
- その他
それぞれについて以下で説明します。
BPF命令列を直接定義する
基本的にマクロを使って命令列を定義する方法です。依存関係が最も少なく、プログラム規模が小さい場合は十分有用です。
Linuxでは、以下にBPFの命令に関する定義があります。
- 「include/uapi/linux/filter.h」
- cBPFのデータ構造定義
- 「include/uapi/linux/bpf_common.h」
- cBPF命令の定義
- 「include/uapi/linux/bpf.h」
- eBPFのデータ構造および命令の定義
- 「include/linux/filter.h」「samples/bpf/bpf_insn.h」
- eBPF命令作成のためのマクロ定義
- この定義はuapiとしてエクスポートされていないため、ユーザー空間から利用したい場合は適宜コピーなどが必要
これらを利用してBPFのプログラムを作成します。
cBPFプログラムの作成
「include/uapi/linux/filter.h」で定義されている、以下のマクロを利用して命令を構成します。
#ifndef BPF_STMT #define BPF_STMT(code, k) { (unsigned short)(code), 0, 0, k } #endif #ifndef BPF_JUMP #define BPF_JUMP(code, k, jt, jf) { (unsigned short)(code), jt, jf, k } #endif
なお前回、cBPFアーキテクチャについて説明した際はcBPF命令の構造体の名称を「struct cbpf_insn」としましたが、Linuxでは「struct sock_filter」という名称になっています。
例として、「arpパケット以外をドロップする」フィルタリングプログラムを作成してみます。引数として、ethernetフレーム先頭を指すポインタが渡されると仮定します。
struct sock_filter code[] = { BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 12), // A = P[12:14] BPF_JUMP(BPF_JMP + BPF_K + BPF_JEQ, 0x806, 0, 1), // accept if A == 0x806, otherwise jmp drop BPF_STMT(BPF_RET + BPF_K, -1), // ret -1 BPF_STMT(BPF_RET + BPF_K, 0), // drop: ret 0 };
やっていることはethernetフレームのtypeフィールドを読み込み、その値が0x806であれば0を返します。そうでなければ-1(実際にはunsignedで評価されるため、unsigned intの最大値)を返します。
戻り値の意味はBPFプログラムをアタッチする箇所で異なりますが、パケットフィルタリングの場合は「戻り値が受容するパケットの最大長」を意味します。
eBPFプログラムの作成
主に「include/linux/filter.h」にあるマクロを利用してプログラムを作成します。eBPF命令の構造体は「sturct bpf_insn」という名称です。
先ほどのcBPFと同じフィルタリングプログラムを作成してみます。R1に「sk_buff」が渡されると仮定します。
struct bpf_insn code[] = { BPF_MOV64_REG(BPF_REG_6, BPF_REG_1), // R6 = R1 BPF_LD_ABS(BPF_H, 12), // R0 = P[12:14] BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0x806, 2), // jmp drop if R0 != 0x806 BPF_MOV64_IMM(BPF_REG_0, -1), // R0 = -1 BPF_EXIT_INSN(), // ret BPF_MOV64_IMM(BPF_REG_0, 0), // drop: R0 = 0 BPF_EXIT_INSN(), // ret };
ここでは「BPF_LD BPF_ABS」命令を利用するために、最初にR6にsk_buffの値を入れています。
BPFアセンブラを利用する
・cBPFアセンブラ
Linuxのソースに含まれるtools/bpfにcBPFのアセンブラが存在します。以下のようにしてアセンブラがコンパイルできます。
% git clone https://github.com/torvalds/linux % cd linux % git checkout -b v4.18 refs/tags/v4.18 # 必要に応じてカーネルバージョンの指定 % cd tools/bpf % make
なお、Linuxのソースリポジトリの全クローンは時間がかかります。gitの履歴が必要でなければ「https://github.com/torvalds/linux/releases」から特定のバージョンをダウンロードできます。
また、bpf toolのコンパイルに当たっては「bison」「flex」「libelf」「bfd」「readline」が必要になります。Ubuntuの場合は以下のようにしてインストールできます。
% sudo apt install bison flex libelf binutils-dev libreadline-dev
先ほど直接作成したBPFプログラムは以下のように書くことができます。
ldh [12] jne #0x806, drop ret #-1 drop: ret #0
このプログラムを「filter.asm」というファイル名で保存します。tools/bpfにあるbpf_asmを利用して以下のようにアセンブルできます。
% ./bpf_asm -c filter.asm { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 1, 0x00000806 }, { 0x06, 0, 0, 0xffffffff }, { 0x06, 0, 0, 0000000000 },
ここで、出力は「struct sock_filter」のコード列になっています。
このアセンブラで利用できるニーモニックの説明はLinuxのドキュメントに存在します("BPF engine and instruction set")。
また、tools/bpfにあるbpf_dbgを使用することで、cBPFプログラムのディスアセンブルや、ブレークポイントありのデバッグ実行などが可能です。この使用方法も上記のドキュメントにあります。
・eBPFアセンブラ
ユーザースペースのeBPF実装であるubpfに、eBPFのアセンブラおよびディスアセンブラが存在します。ubpfのeBPFアセンブラで利用されるニーモニックはこちらで定義されています。
これまでと同様のプログラムは以下のように書くことができます。
ldxh r2, [r1+12] mov r0, 0 jne r2, 0x0608, +1 mov r0, -1 exit
なお、このプログラムではR1レジスタに渡された引数がパケット先頭のポインタを持つものとしています(ubpf VMでは「LD_ABS」「LD_IND」命令をサポートしていません)。
以下のようにしてコンパイルできます。
% git clone https://github.com/iovisor/ubpf; cd ubpf % ./bin/ubpf-assembler filter.asm a.out % hexdump -C a.out 00000000 69 12 0c 00 00 00 00 00 b7 00 00 00 00 00 00 00 |i...............| 00000010 55 02 01 00 08 06 00 00 b7 00 00 00 ff ff ff ff |U...............| 00000020 95 00 00 00 00 00 00 00 |........|
ubpfアセンブラの出力はバイナリになります。
なお、もともとこのアセンブラは、ubpf VMのテスト用途に作成されたものです。Linuxで使用されるeBPFプログラム生成を目的にしたものではありません。
tcpdump(libpcap)でフィルター式をcBPFにコンパイルする
tcpdump(libpcap)には高レベルのフィルター式をcBPFにコンパイルする機能があります。フィルター式に関するドキュメントは「Manpage of PCAP-FILTER」にあります。
「tcpdump -d」を利用してフィルター式をコンパイルできます。例えば、これまでと同じようにarpのみ受け付けるフィルターは以下のようになります。
% tcpdump -d arp (000) ldh [12] (001) jeq #0x806 jt 2 jf 3 (002) ret #262144 (003) ret #0
また「tcpdump -dd」で「struct sock_filter」のコード列を得ることができます。
% tcpdump -dd arp { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 1, 0x00000806 }, { 0x6, 0, 0, 0x00040000 }, { 0x6, 0, 0, 0x00000000 },
Clang(LLVM)でCのプログラムをeBPFにコンパイルする
LLVM 3.7からバックエンドとしてeBPFが追加され、Clangを利用してCのプログラムをeBPFへコンパイルすることが可能になりました。これが現在BPFプログラムを作成するための最も主流な方法です。
今までと同様のプログラムは以下のように書くことができます。
なお、ここでは簡単にするために引数としてパケットを直接受け取っていますが、実際のパケットフィルタリングではeBPFに渡される引数は「struct __sk_buff」です(このデータ構造はカーネルで実際に利用される「sk_buff」ではなく、疑似的なデータ構造です。このことの詳細は検証器の説明の際に再び触れます)。
int f(char *packet) { short type = *(short *)(packet + 12); if (type == 0x0608) { // big endian return -1; } return 0; }
以下のコマンドでコンパイルします。結果はelfバイナリになります。
% clang -O3 -c -target bpf -o filter.o filter.c % readelf -x .text filter.o Hex dump of section '.text': 0x00000000 69110c00 00000000 b7000000 ffffffff i............... 0x00000010 15010100 08060000 b7000000 00000000 ................ 0x00000020 95000000 00000000 ........
先ほどのubpfのdisassenblerを利用することで、アセンブリのコードが確認できます。
% objcopy -I elf64-little -O binary filter.o filter.bin % ./ubpf/bin/ubpf-disassembler filter.bin filter.s % cat filter.s ldxh r1, [r1+12] mov r0, 0xffffffff jeq r1, 0x608, +1 mov r0, 0x0 exit
注意点として、CのプログラムがeBPFへコンパイルできることと、そのeBPFプログラムがカーネル内の検証器をパスするかどうかは全くの別問題です。LinuxでeBPFを動作させるためにはC側で考慮しなければならないことが多々存在します。
また、ClangでコンパイルしたオブジェクトはELFバイナリとなりますが、実際の利用に当たってはこのELFバイナリをロードするためのローダーが必要になります。
CからのeBPFプログラム作成をサポートするために幾つかツールやライブラリが用意されています。CによるeBPFプログラムの作成は次回以降、より詳しく説明します。
その他
より簡単にBPFのプログラムが作成できるように、幾つかのプロジェクトではDSLからBPFへのコンパイルを行っています。
- bpftrace、ply
dtraceに似た言語からeBPFへコンパイル(トレーシング用途) - P4
パケット処理を記述するためのプロトコル・ターゲット非依存な言語。バックエンドとしてeBPFをサポートしており、P4からeBPFへのコンパイルが可能(正確にはいったんP4からeBPFへコンパイル可能なCへ変換する) - kafel
ポリシー記述から対応するseccomp filter用のBPFプログラムの作成(こちらもC言語に変換する)
また、DSLとは少々異なりますが、BCCではBPFプログラムを簡単に作成するためのmodified Cを提供します。
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カーネル側のシステムコールを見ていく。