BCC(BPF Compiler Collection)によるBPFプログラムの作成Berkeley Packet Filter(BPF)入門(6)(2/2 ページ)

» 2019年12月17日 05時00分 公開
[味曽野雅史OSSセキュリティ技術の会]
前のページへ 1|2       

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によるトレーシングについて、詳しく取り上げる予定です。

筆者紹介

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

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

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


前のページへ 1|2       

Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

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

メールマガジン登録

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