BCC(BPF Compiler Collection)によるBPFプログラムの作成:Berkeley Packet Filter(BPF)入門(6)(2/2 ページ)
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、BCC(BPF Compiler Collection)によるBPFプログラムの作成について。
BPFマップの定義
(1)で「BPF_MAP_TYPE_ARRAY」のBPFマップを定義しています。特にここでは、「BPF_ARRAY({マップ名},{valueの型},{エントリ数})」を指定しています。「BPF_ARRAY」のキーは4バイトと決まっているので、キーのサイズを指定する項目はありません。
BCCのコンパイラはこの記述を見つけると記述内容に従ってBPFマップを作成し、またBPFプログラム中でそのBPFマップを利用している箇所にBPFマップのファイルディスクリプタを埋め込みます。
BPFマップの定義方法に関してはこちらにドキュメントがあります。
パケットデータへのアクセス
「cursor_advance()」でパケットデータのポインタを求め、アクセスしています。コードにはどこにもskbのデータにアクセスするような箇所はないので、やや不思議な処理ですが、これもBCCコンパイラで特別扱いされ、最終的にはこのコードは「BPF_LD | BPF_ABS」を利用するコードにコンパイルされます。
BCCで「BPF_PROG_TYPE_SOCKET_FILTER」のBPFプログラムを作成する際に、パケットデータにアクセスするには「cursor_advance()」を利用する、と覚えておけばいいでしょう。
BPFマップの参照
BPFプログラム内から、「my_map.lookup()」という形でBPFマップにアクセスしています。これもBCC固有の処理で、最終的にはBPFマップにアクセスするヘルパー関数呼び出しに変換されます。
BPFマップに対してこのように利用できる関数はこちらにまとまっています。
BPFプログラムのロード
Python側からは、「bpf = bcc.BPF(prog=text)」でBPFプログラムをコンパイルします。
もしBPFプログラムを別のファイルに記述している場合は、「bcc.BPF(src_file="prog.c")」という形で読み込むことができます。
さらに、「bpf.load_func("bpf_prog", BPF.SOCKET_FILTER)」でソースの中の関数名を指定して、そのBPFプログラムをカーネル内にロードします。今回はソースの中に1つの関数しか定義していませんが、複数の関数を定義し、複数のBPFプログラムをロードすることも可能です。
ユーザー空間からのBPFマップの参照
BPFプログラムで作成したBPFマップは、Python側からは「my_map = bpf.get_table("my_map")」のようにして取得できます。今回作成したBPFマップはキーがint型の「BPF_ARRAY」なので、Python側からは「my_map[ct.c_int(PROTO["TCP"])].value」のようにしてアクセスしています。
ライセンス
BPF Cのソース内で、「#define BPF_LICENSE」を利用してBPFプログラムをロードする際のライセンスが指定できます。
もしこの指定がない場合はデフォルトでGPLが適用されます。BPFヘルパー関数の一部はBPFプログラムがGPLでなければ利用できません。BPFヘルパー関数とライセンスの関係はこちらにまとまっています。
ちなみに、BCC自体のライセンスはApache-2です。BCCツールのBPFプログラムはGPLとしてロードされます。
BCCとカーネルヘッダ
BCCはデフォルトで現在動作中のカーネルのバージョンに一致したカーネルヘッダを要求します。BCCが参照するカーネルソースのディレクトリや、カーネルバージョンは環境変数により制御可能です。
詳しくはこちらを参照してください。
プログラムのデバッグ
「bcc.BPF(debug=debug)」でフラグを指定することで、デバッグ出力が有効になります。具体的には、以下の内容が確認可能です。
名称 | 値 | 意味 |
---|---|---|
DEBUG_LLVM_IR | 0x1 | LLVM IR |
DEBUG_BPF | 0x2 | BPFのバイトコード |
DEBUG_PREPROCESSOR | 0x4 | プリプロセッサの出力 |
DEBUG_SOURCE | 0x8 | BPFのバイトコード+ソース |
DEBUG_BPF_REGISTER_STATE | 0x10 | BPFのバイトコード+レジスタの状態 |
DEBUG_BTF | 0x2 | BTFの情報 |
例えば、以下が生成されるBPFプログラムです。
$ sudo python3 sockex.py --debug 0x8 Disassembly of section .bpf.fn.bpf_prog: bpf_prog: ; { // Line 25 0: bf 16 00 00 00 00 00 00 r6 = r1 ; if (skb->pkt_type != PACKET_OUTGOING) // Line 31 1: 61 61 04 00 00 00 00 00 r1 = *(u32 *)(r6 + 4) 2: 55 01 0c 00 04 00 00 00 if r1 != 4 goto +12 3: 28 00 00 00 0c 00 00 00 r0 = *(u16 *)skb[12] ; if (!(bpf_dext_pkt(skb, (u64)ethernet+12, 0, 16) == 0x0800)) { // Line 35 4: 55 00 0a 00 00 08 00 00 if r0 != 2048 goto +10 5: 30 00 00 00 17 00 00 00 r0 = *(u8 *)skb[23] ; index = bpf_dext_pkt(skb, (u64)ip+9, 0, 8); // Line 40 6: 63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0 ; value = bpf_map_lookup_elem((void *)bpf_pseudo_fd(1, -1), &index); // Line 42 7: 18 11 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 ld_pseudo r1, 1, 4294967295 10: bf a2 00 00 00 00 00 00 r2 = r10 10: 07 02 00 00 fc ff ff ff r2 += -4 11: 85 00 00 00 01 00 00 00 call 1 ; if (value) // Line 43 12: 15 00 02 00 00 00 00 00 if r0 == 0 goto +2 ; lock_xadd(value, skb->len); // Line 44 13: 61 61 00 00 00 00 00 00 r1 = *(u32 *)(r6 + 0) 14: db 10 00 00 00 00 00 00 lock *(u64 *)(r0 + 0) += r1 ; } // Line 47 15: b7 00 00 00 00 00 00 00 r0 = 0 16: 95 00 00 00 00 00 00 00 exit
なお、ここで出力されるBPFコードは、カーネルにロードされる前のBPFコードです。カーネルはBPFプログラムをロードする際に、検証器の中で一部コードを書き換える場合があります。カーネル内で動作しているBPFプログラムは「bpftool」で確認できます。
BCCとBPFプログラムのコンパイル
ここでの例から明らかなように、BCCではプログラム(上の例だと「sockex.py」)実行のたびにBPFプログラムをコンパイルします。
これには一長一短があります。
欠点
明らかな欠点としては逐一コンパイルするのでプログラムの開始が遅くなることです。
また、コンパイルするためのコンパイラ(LLVM)を実行マシンに用意する必要があります。
さらに、もしコンパイルエラーがあっても、それは実行時に初めて知ることになります。
利点
一方で、プログラムの変更が容易であるという利点があります。例えば、パケットフィルタリングで特定のIPアドレスのパケットだけをフィルタリングしたい場合、コマンドライン引数からIPアドレスを受け取り、それをBPFプログラムに埋め込むといった処理が容易に行えます。
また、カーネルトレーシングを実施する場合は、カーネルバージョンに依存したデータ構造にアクセスすることがあります。この場合はカーネル更新のたびにBPFプログラムをコンパイルする必要があります。
BCCは実行時にカーネルのヘッダを利用してコンパイルするので、BPFプログラムが古いデータ構造を利用するといった問題が生じません。BCCではプログラムの柔軟性や拡張しやすさを重視してこのような設計になっています。
特にプロダクション環境では、前述した欠点が大きく問題になることがあります。
そこで最近BCCでコンパイル済みのBPFプログラムをロード可能にしようという議論があります(例)。
ここで、もしカーネルバージョンが異なっていてもBPFプログラムが動作できるように、「BTF(BPF Type Format)」と呼ばれる型情報を利用して、BPFプログラムをロードする際にカーネルに対応するように書き換える、ということが2019年の「Linux Plumbers Conference」で提案されています(発表資料)。
現時点でコンパイル済みのBPFプログラムをロードするには「libbpf」や「gobpf」のELFローダーを利用する方法があります。
BCCとlibbpf
BCCは内部で前回紹介した「libbpf」を利用しています。
libbpfの関数はPython側から「lib.bcc.*」でアクセスできます。BCCが直接対応していない機能や、bpfシステムコールの挙動を確認したいときなどに便利です。どんな関数が利用可能かは例えばこちらから確認できます。
まとめ
今回はBCCの概要と、それを利用したソケットに対するBPFプログラムの作成方法を説明しました。
BCCについてより詳しく知るには、公式のレファレンスガイドや、チュートリアルが参考になります。ただし、これらは主にBPFによるトレーシングを対象にしています。
次回は、このBPFによるトレーシングについて、詳しく取り上げる予定です。
筆者紹介
味曽野 雅史(みその まさのり)
東京大学 大学院 情報理工学系研究科 博士課程
オペレーティングシステムや仮想化技術の研究に従事。
- メール:misono(at)os.ecc.u-tokyo.ac.jp
- ブログ:http://mmi.hatenablog.com/
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カーネル側のシステムコールを見ていく。