BCC(BPF Compiler Collection)によるBPFプログラムの作成:Berkeley Packet Filter(BPF)入門(6)(1/2 ページ)
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、BCC(BPF Compiler Collection)によるBPFプログラムの作成について。
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載「Berkeley Packet Filter(BPF)入門」。今回はBPFプログラム作成のための代表的なライブラリである「BCC(BPF Compiler Collection)」を紹介します。
BCCを利用することで、簡単にトレーシングやネットワーク処理のBPFプログラムを作成することができます。さらに、BCCにはBPFを利用したさまざまなトレーシングツールが含まれているので、一般のツール利用者にとってもBCCは有用です。
BPF Compiler Collection(BCC)とは
BCCはBPFによるプログラム作成を支援するためのライブラリおよび、それを利用したツール群です。
「IOVisor」というLinux Foundationのプロジェクトにより開発されています。BPFプログラム作成のためのライブラリは複数存在しますが、その中でもBCCは代表的な存在です。
BCCのコア部分はC++で記述されていますが、同じ開発リポジトリ上でPythonおよびLuaのバインディングが開発されています。また、Go言語やRustバインディングもあります。一般にBCCはPythonから利用されることが多く、BCCリポジトリに含まれるツールも多くはPythonで記述されています。
本稿でもPythonでBCCを利用します。
BCCの機能
ライブラリとしてのBCCは以下の機能を提供します。
- BPFプログラムを簡単に記述するためのModified C(BPF C)
- BPF Cのコンパイル機能
- BPFプログラムローダー
- BPFマップへアクセスするための関数
BCCでは、「BPF C」と呼ばれる、Cの方言でBPFプログラムを作成します。BPF Cは基本的にはC言語そのものですが、BPFマップの定義などのBPF固有の処理が書きやすくなっています。BCCのプログラムローダーは、「LLVM/Clang」を用いてBPF CのAST (Abstract Syntax Tree)を解析、変更した上でBPFプログラムにコンパイルし、カーネルにロードします。
BCCでは主に以下のBPFプログラムの作成に用いられています(ただし、この限りではありません)。
- トレーシング(「BPF_PROG_TYPE_KPROBE」「BPF_PROG_TYPE_TRACEPOINT」「BPF_PROG_TYPE_PERF_EVENT」)
- ソケットフィルター(「BPF_PROG_TYPE_SOCKET_FILTER」)
- XDP(「BPF_PROG_TYPE_XDP」)
コラム C言語以外によるBPFプログラムの作成
BCCにはさまざまな言語によるバインディングが存在しますが、あくまでBPFプログラム自体はC(BPF C)によって記述します。このことは残念に思われるかもしれませんが、BPFプログラムは基本的にLinuxカーネル内のデータ構造にアクセスするので、カーネルと同じC言語の方がいろいろと都合の良いことが多いです。
幾つか、C言語以外からBPFプログラムを作成する試みがあります。特にBPFによるトレーシング用途として「bpftrace」「ply」といったDTraceライクなDSLが開発されています。内部的には、bpftraceはLLVMおよびBCCを利用し、plyはLLVMを利用せず、直接BPFコードを生成します。
またPythonのバイトコードを直接BPFプログラムに変換する「py2bpf」といったものもあります。
BCCのインストール
多くのパッケージマネジャーがデフォルトでBCCを取り扱っています。こちらにディストリビューションに応じたBCCのインストール方法が記載されています。
以下にUbuntu 18.04.3に対するインストール方法を記します。
公式のリポジトリからのインストール
BCCは公式のリポジトリからインストールすることが可能です。
sudo apt-get install bpfcc-tools python3-bpfcc linux-headers-$(uname -r)
「/usr/sbin/」以下に「-bpfcc」というpostfixでBCCのツールがインストールされます。
IOVisorのリポジトリからのインストール
IOVisorのリポジトリを利用すると、BCCのnightly-buildが導入できます。
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD echo "deb [trusted=yes] https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs)-nightly main" | sudo tee /etc/apt/sources.list.d/iovisor.list sudo apt update sudo apt install python3-bcc bcc-tools libbcc-examples
「/usr/share/bcc/tools/」以下にBCCのツールがインストールされます。
Ubuntuの公式リポジトリにあるものと、IOVisorが提供するもので名前が異なる点に注意してください。問題を避けるためにインストールするのはどちらか片方のものにするのがいいでしょう。
Dockerの利用
Dockerを利用してBCCを試すこともできます。以下でBCCがインストール済みのコンテナを起動できます。
docker run -it --rm \ --privileged \ -v /lib/modules:/lib/modules:ro \ -v /usr/src:/usr/src:ro \ -v /etc/localtime:/etc/localtime:ro \ --workdir /usr/share/bcc/tools \ zlim/bcc
BCCインストールの確認
以下のコマンドが成功すればBCCのpythonバインディングを利用することができます。
sudo python3 -c "import bcc"
BCCによるパケットトレースプログラムの作成
ここからは、BCCを用いてBPFプログラムを作成します。
BCCリポジトリの「examples」にBCCの使用例が、また「tools」にBCCを利用したトレーシングツールがあります。
BCCのメインはカーネルトレーシングですが、ここでは前回に続いてソケットに対するBPFプログラムを作成してみます。カーネルトレーシングについては次回詳しく触れます。
前回利用したパケットトレースプログラム(「sockex1_user.c」「sockex1_kern.c」)と同等のものを、BCCを用いて作成してみます。作成するBPFプログラムは、下図のようにソケットに対してアタッチされ、IPv4のプロトコルタイプ別に受信パケットサイズを記録します。
以下にBCCによるプログラムを示します。BPF C固有の記述箇所に関しては★印を付けています。
#!/usr/bin/env python # -*- cofing: utf-8 -*- from __future__ import print_function import ctypes as ct import os import subprocess import time from bcc import BPF prog = r""" // SPDX-License-Identifier: GPL-2.0+ #define BPF_LICENSE GPL #include <uapi/linux/if_ether.h> #include <uapi/linux/if_packet.h> #include <uapi/linux/ip.h> #include <net/sock.h> #include <bcc/proto.h> // ★(1)BPFマップの定義 BPF_ARRAY(my_map, long, 256); int bpf_prog(struct __sk_buff *skb) { int index; long *value; u8 *cursor = 0; if (skb->pkt_type != PACKET_OUTGOING) return 0; // ★(2)パケットデータへのアクセス struct ethernet_t *ethernet = cursor_advance(cursor, sizeof(*ethernet)); if (!(ethernet->type == 0x0800)) { return 0; } struct ip_t *ip = cursor_advance(cursor, sizeof(*ip)); index = ip->nextp; // ★(3)BPFマップのアクセス value = my_map.lookup(&index); if (value) lock_xadd(value, skb->len); return 0; } """ PROTO = { "ICMP": 1, "TCP": 6, "UDP": 17, } def main(interface="lo", debug=0): # (4)BPFプログラムのロード bpf = BPF(text=prog, debug=debug) bpf_prog = bpf.load_func("bpf_prog", BPF.SOCKET_FILTER) # (5)BPFプログラムのアタッチ BPF.attach_raw_socket(bpf_prog, interface) my_map = bpf.get_table("my_map") devnull = open(os.devnull, "w") p = subprocess.Popen( ["/bin/ping", "-4", "-c5", "localhost"], stdout=devnull) for _ in range(5): # (6) BPFマップへのアクセス print("TCP {} UDP {} ICMP {} bytes".format( my_map[ct.c_int(PROTO["TCP"])].value, my_map[ct.c_int(PROTO["UDP"])].value, my_map[ct.c_int(PROTO["ICMP"])].value)) time.sleep(1) p.wait() if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("--interface", default="lo") parser.add_argument("--debug", default=0) args = parser.parse_args() main(args.interface, args.debug)
このBPFプログラムは、受信したパケットについてIPヘッダのプロトコル番号別にパケットサイズを計数します。以下に実行結果を示します。
$ sudo python3 sockex.py TCP 0 UDP 0 ICMP 0 bytes TCP 0 UDP 0 ICMP 196 bytes TCP 0 UDP 0 ICMP 392 bytes TCP 0 UDP 0 ICMP 588 bytes TCP 0 UDP 0 ICMP 784 bytes
このように、Pythonバインディングを利用したBCCでは、BPF CでBPFプログラムを記述し、それ以外のBPFプログラムのロードやBPFマップのアクセスなどはPythonから行います。
次ページで要点を説明します。
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カーネル側のシステムコールを見ていく。