第688回 と第690回 では、カーネルのトレーシングツールとして注目されているeBPFを活用するためのツールとしてBCC を紹介しました。 また第692回 ではBCC以外のeBPFを活用したツールの利用方法も紹介しています。
今回は一般的なコンパイラのようにバイナリを生成でき、移植性が高く、そして近い将来eBPFを使うための本流のひとつとなりそうなBPF CO-REについて紹介しましょう。
BPF CO-REの登場
BPF CO-RE(Compile Once - Run Everywhere)については第692回 でも軽く紹介しました。改めてまとめると、次のような機能を実現する仕組みです。
BPFを利用したバイナリを実行環境とは異なる環境でビルドできる
カーネルバージョン間の違いもある程度は吸収してくれる
実行バイナリのサイズはそれなりに小さくなる
コンテナ内部などターゲットと異なるカーネルが動いている環境でもビルドできる
つまりステージング環境や開発環境でビルド&テストしたバイナリを、プロダクション環境に配布・実行できるということです。特にモニタリングやデバッグ用途で、できるだけプロダクション環境をいじることなくeBPFバイナリを活用したい場合に、非常に便利な仕組みとなります。
実はBCCでは、大半のツールにもBPF CO-RE版 のコードが用意されています。将来的にPython版は廃止して、libbpfを利用したBPF CO-RE版に全面的に移行する予定のようです。つまりこのコードを参考にすればどんな風に実現すれば良いかがすぐにわかるでしょう。
ただしBPF CO-REを使用するためには、比較的新しいカーネルやClang/LLVMなどのツールチェイン、それに実行したバイナリをロードするためのlibbpf が必要になります。特にeBPFのポータブル性を実現するためには、実行中のカーネルの内部情報を提供する「BPF Type Format(BTF) 」のサポートが重要になってくるのです。
Ubuntuの場合、最初にBPF CO-REが動くようになったのは、20.10の頃になります。つまり現時点で最新のLTSであるUbuntu 20.04 LTSでは、BPF CO-REの恩恵は受けられません。また、BPF/BTF自体がまだ活発に機能拡張されているため、本格的に使うならより新しいカーネルやツールチェインを使いたいところです。よってUbuntuの場合は、2022年4月にリリースされる次のLTSであるUbuntu 22.04 LTSぐらいから、プロダクション用途でも使われるようになっていくのではないでしょうか[1] 。
たとえばUbuntu 21.10のKernel 5.13における、BPF/BTF関連のカーネルコンフィグは次のようになっています。
$ grep -E "(BPF|BTF)[_= ]" /boot/config-$(uname -r)
CONFIG_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y
# BPF subsystem
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_BPF_UNPRIV_DEFAULT_OFF=y
# CONFIG_BPF_PRELOAD is not set
CONFIG_BPF_LSM=y
# end of BPF subsystem
CONFIG_CGROUP_BPF=y
CONFIG_IPV6_SEG6_BPF=y
CONFIG_NETFILTER_XT_MATCH_BPF=m
CONFIG_NET_CLS_BPF=m
CONFIG_NET_ACT_BPF=m
CONFIG_BPF_STREAM_PARSER=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_VIDEO_SONY_BTF_MPX=m
CONFIG_DEBUG_INFO_BTF=y
CONFIG_PAHOLE_HAS_SPLIT_BTF=y
CONFIG_DEBUG_INFO_BTF_MODULES=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_TEST_BPF=m
ちなみにUbuntu 21.04以降は、libbpfが最初からインストールされるようになりました。これはiproute2がlibbpfをサポートするようになった ためで、それに合わせてlibbpfもデスクトップ版・サーバー版ともに最初からインストールされていることを期待できます。
今回はUbuntu 21.10ベース(libbpf v0.4)で話を進めます。また22.04ではv0.5以降が採用される見込みです。将来的にUbuntu 22.04 LTSに移行する予定がある場合は、今のうちに21.10や現在開発中のJemmyを試しておくと良いでしょう。
BPF CO-REのコンパイル環境の準備
今回も第690回 と同じように、システムコールであるexecve()
をトレースするコードを題材として使いましょう。ちなみに第692回 ではsysfs以下を使ってツールレスに同等の機能を実現したり、bpftraceでより簡単に実現する方法も紹介しています。興味のあるかたはそちらも参照してください。
BPF CO-REのバイナリをコンパイルするには、これまでよりも若干複雑な手順を踏む必要があります。
bpftool
コマンドでターゲットとなるカーネルバージョンのvmlinux.h
を入手する
ターゲットのカーネルにロードされるBPFプログラムをClangでコンパイルしてBPFオブジェクトファイルを生成する
そのBPFオブジェクトファイルから、bpftool
コマンドを用いてユーザーランドのプログラムで使うためのスケルトンヘッダーファイルを生成する
スケルトンヘッダーファイルを使って、ユーザーランドのプログラムをコンパイルする
簡単に言うと、BCCやbpftraceがこれまで肩代わりしてくれた諸々を、自分でもやる必要があるわけです。
まずはビルドに必要なツール一式をインストールしておきましょう。
$ sudo apt install build-essential libbpf-dev clang llvm linux-tools-generic
前半はコンパイルに必要なツール群です。最後のlinux-tools-genericパッケージはビルドに使うbpftool
コマンドと、実行中のカーネル固有の設定を取得するためのライブラリがセットになったパッケージです。このうちbpftool
コマンドは、linux-tools-commonパッケージが提供していますので、bpftool
のカーネルバージョンに依存しないサブコマンドだけを使うなら、こちらのパッケージだけインストールすると良いでしょう。
今回は最低限のコードのみを紹介します。必要なのは「BPFプログラムのコード」と「それをロードするためのコード」の2ファイルです。
BPFプログラムのコード
まずはBPFプログラムのコード(execsnoop.bpf.c
)は次のようになります。
/* SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause */
/* SPDX-FileCopyrightText: 2021 Mitsuya Shibata */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
SEC("tracepoint/syscalls/sys_enter_execve")
int syscalls__execve(void *ctx)
{
bpf_printk("Hi, execve!\n");
return 0;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC()
はlibbpfのマクロです。これ自体は引数や関数をELFバイナリの中の指定したセクションに配置だけのマクロで、具体的には「__attribute__((section(名前), used))
」が設定されます。このセクションはBPFをカーネルにロードする際に必要になるものです。
最初のtracepoint/syscalls/sys_enter_execve
セクションには、その関数のプログラムが置かれます。こちらのセクションはトレース対象の関数に合わせて名前を付けなくてはなりません。
tracepoint:/sys/kernel/debug/tracing/events/
以下にあるイベント名をもとに「tracepoint/イベント名
」「 tp/イベント名
」
kprobe、kretprobe:「kprobe/関数名
」
こんな感じで設定していきます。今回はsyscalls/sys_enter_execve
イベントなので「tracepoint/syscalls/sys_enter_execve
」としました。他にも様々な指定方法が存在 します。用途に合わせて使い分けると良いでしょう。
関数名のほうは任意の名前が使えます。今回は第690回 の例に合わせてみました。関数の中でやっていることは「bpf_printk()
」でトレースバッファーにテキストを出力しているだけです。これは第690回 で紹介した「bpf_trace_printk()
」のラッパー関数です。このように、BPFのコード部分はBCCでコンパイルしている部分と実質同じであるため、BCCのドキュメントであるBPF C の記述が参考になります[2] 。
最後のlicense
セクションは、BPFオブジェクトのライセンスを設定しています。これは事実上、必須のセクションです。LinuxカーネルはGPL-2.0ライセンスが採用されています。ただし、カーネルモジュールに関しては歴史的経緯から、GPLと互換性のないライセンスのコードで作られたモジュールであってもロードはできます。しかしながら、一部のカーネルシンボルを利用する場合は、GPLかGPLと互換性のあるライセンス であることが必須になっています。
これはカーネル内部にロードされるBPFオブジェクトであっても同じで、GPL専用のシンボルを使う場合は未指定だったり、GPLと互換性のあるライセンスでないと、ロード時に次のようなエラーが表示されます。
cannot call GPL-restricted function from non-GPL compatible program
ここでは3条項BSDとGPL 2.0のデュアルライセンスにしていますが、基本的にカーネルモジュールやBPFのコードはGPL-2.0にまとめてしまうのが安全です[3] 。
BPFプログラムからオブジェクトとスケルトンヘッダーの生成
次にBPFプログラムからClangでBPFオブジェクトを作成し、それをユーザーランドプログラムから参照できるスケルトンヘッダーファイルに変換しましょう。
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
$ clang -g -O2 -Wall -target bpf -D__TARGET_ARCH_x86 -c execsnoop.bpf.c -o execsnoop.bpf.o
$ bpftool gen skeleton execsnoop.bpf.o > execsnoop.skel.h
libbpf: elf: skipping unrecognized data section(4) .rodata.str1.1
まずターゲットカーネルのvmlinux.h
をbpftool
コマンドで生成します。このvmlinux.h
はカーネル内部の構造体定義を列挙したファイルで、BPFプログラムの中でincludeしています。これによりBPFプログラムはどの構造体にどのようにアクセスすれば良いか把握できます。
vmlinux.h
自体はカーネルのバージョンに紐付いています。このためバージョンによっては構造体の中身が変わることがあります。しかしながらlibbpfがBPFオブジェクトをロードする際に、多少の変更点はよろしく対応してくれるのです[4] 。まずは上記のように実行中のカーネルから生成したデータを参照すると良いでしょう。
[4] たとえばBPF CO-RE版のlibbpfは、amd64向けにはKernel 5.5のvmlinux.h を使っています。たとえばKernel 5.13を採用したUbuntu 22.10であっても、このvmlinux.h
を使ってビルドしたBPFバイナリはきちんと動作します。ただしlibbpfが吸収できない差異があった場合は、ロード時に失敗することになります。
話をもとに戻すと、vmlinux.h
を生成したら次はclang
コマンドによるコンパイルでBPFオブジェクトを生成します。「 -target bpf
」がポイントです。また「-D__TARGET_ARCH_x86
」を指定していますが、これはlibbpfやカーネル側で定義されたリスト にしたがって設定する必要があります。
生成されたBPFオブジェクトを見ると、SEC()
マクロで指定したセクションが作られていることがわかります。
$ readelf -t execsnoop.bpf.o | grep -E "tracepoint|license"
[ 2] tracepoint/syscalls/sys_enter_execve
[ 4] license
このBPFオブジェクトをユーザーランドプログラムで使いやすいようにC言語のヘッダーファイルへと変換してくれるのが、「 bpftool gen skeleton
」です。BPFオブジェクトをロード・適用するためのヘルパー関数だけでなく、BPFオブジェクトそのものもASCII化してくれます。よってユーザーランドプログラム側はこのスケルトンヘッダーファイルさえあれば、BPF CO-REバイナリを生成できます。
ちなみにスケルトンヘッダーファイルの各シンボルの命名規則は「BPFオブジェクトのファイル名__シンボル名
」となります。たとえば今回のように「execsnoop.bpf.o
」の場合は「execsnoop.bpf
」のうちピリオドをアンダースコアに変換し、関数名の「open
」を追加して「execsnoop_bpf__open
」のような名前が生成されます。また、BPFオブジェクトを保有している構造体は「struct execsnoop_bpf
」となります。もしファイル名以外の特定の名前を指定したい場合は「bpftool gen skeleton execsnoop.bpf.o name 名前 > execsnoop.skel.h
」のように「name 名前
」オプションを追加してください。
スケルトンヘッダーファイルを生成時に次のような警告メッセージが表示されいてます。
libbpf: elf: skipping unrecognized data section(4) .rodata.str1.1
これはlibbpfが古いことによるもので、v0.5より新しいリリースでは修正予定 です。現状では無視してかまいません。
ここまででBPFオブジェクト側の準備が整いました。
異なるカーネルのvmlinux.h
を生成するには
bpftool
プログラムはvmlinux
ファイルからもvmlinux.h
を生成してくれます。もしamd64マシンの上から異なるアーキテクチャーのvmlnux.h
を生成したいなら次のようにデバッグシンボル付きパッケージを取得・生成することも可能です。
$ wget http://ddebs.ubuntu.com/pool/main/l/linux/linux-image-unsigned-5.15.0-12-generic-dbgsym_5.15.0-12.12_amd64.ddeb
$ dpkg-deb -x linux-image-unsigned-5.15.0-12-generic-dbgsym_5.15.0-12.12_amd64.ddeb .
$ bpftool btf dump file usr/lib/debug/boot/vmlinux-5.15.0-12-generic format c > vmlinux.h
デバッグシンボル付きパッケージは、通常のリポジトリは異なるリポジトリに置かれています[5] 。このうちamd64/arm64/ppc64el/s390xのカーネルパッケージはmain/l/linux 以下にありますし、他にも各クラウド向けカーネルなどはmain/l/linux-aws 、main/l/linux-azure 、main/l/linux-gcp などに配置されています。また、main/l/linux-riscv のようにサポートされたばかりのアーキテクチャーは別ディレクトリになっていることもあるため、とりあえずは「main/l/」以下を眺めてみると良いでしょう[6] 。
もし実機があるなら次の方法でダウンロードURLを取得できます。
$ apt download --print-uris linux-image-unsigned-$(uname -r)
'http://jp.archive.ubuntu.com/ubuntu/pool/main/l/linux/linux-image-unsigned-5.13.0-21-generic_5.13.0-21.21_amd64.deb' (略)
ここの「pool/」以下が「ddebs.ubuntu.com」のそれに一致します。なおデバッグシンボル付きパッケージは、数GBのサイズなのでダウンロードする際はネットワークやストレージの状態に注意しましょう。
ユーザーランドプログラムの作成
次に、BPFオブジェクトをロード・適用するための、ユーザーランドプログラム(execsnoop.c
)を次のように作成します。
/* SPDX-License-Identifier: CC0-1.0 */
#include <stdio.h>
#include <unistd.h>
#include "execsnoop.skel.h"
int main(void)
{
struct execsnoop_bpf *obj;
obj = execsnoop_bpf__open();
if (!obj) {
fprintf(stderr, "failed to open BPF object\n");
return 1;
}
if (execsnoop_bpf__load(obj)) {
fprintf(stderr, "failed to load BPF object\n");
goto cleanup;
}
if (execsnoop_bpf__attach(obj)) {
fprintf(stderr, "failed to attach BPF object\n");
goto cleanup;
}
for (;;) {
sleep(1);
}
cleanup:
execsnoop_bpf__destroy(obj);
return 0;
}
これが実質最低限必要なプログラムです。このうち「execsnoop_bpf
」で始まる名前はすべて、スケルトンヘッダーファイルとして自動生成されたものです。上記で実施しているのは次のような処理です。
FOO__open()
でBPFオブジェクトをメモリ上に展開します。一度成功したあとは、不要になったらFOO__destroy()
で破棄する必要があります。
FOO__load()
でBPFオブジェクトの検証が行われます。コードに問題があった場合は、だいたいはここでエラーになります。
FOO__attach()
でカーネルにアタッチします。実際にBPFプログラムが動き出すのはこのタイミングです。
FOO__open()
とFOO__load()
はまとめて実行するFOO__open_and_load()
も用意されているので、普段はそちらを使っても良いでしょう。
今回は「bpf_printk()
」でトレースバッファーに書き出すためのプログラムです。そこでユーザーランドプログラム側は、アタッチしたあとは単に無限ループでBPFプログラムが動き続けるのを眺めているだけになります。
ユーザーランドプログラムをビルドしましょう。こちらはGCCなりClangなり、好みのコンパイラーを使えます。
$ cc -g -O2 -Wall -c execsnoop.c -o execsnoop.o
$ cc -g -O2 -Wall execsnoop.o -lbpf -o execsnoop
$ ldd execsnoop
linux-vdso.so.1 (0x00007fff2413c000)
libbpf.so.0 => /lib/x86_64-linux-gnu/libbpf.so.0 (0x00007f83d6ece000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f83d6ca6000)
libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x00007f83d6c88000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f83d6c6c000)
/lib64/ld-linux-x86-64.so.2 (0x00007f83d6f2b000)
やっていることは単純です。ldd
の実行結果だとlibelfやlibzをリンクしていますが、これはlibbpf側の依存関係によるものです。これらのライブラリはいずれも原則的にどのUbuntuにもインストールされていると思って良いため、ライブラリのバージョンが合う限りにおいては、作られたバイナリファイルだけを別のUbuntuに持っていって実行することが可能というわけです。
実際に実行してみる
今回のコードは、単にトレースバッファーに書き込んでいるだけです。よって実行しても何も表示されません。結果を確認したい場合は、別の端末からトレースバッファーを表示しましょう。まずはexecsnoop
を実行します。
$ sudo ./execsnoop
libbpf: elf: skipping unrecognized data section(4) .rodata.str1.1
BPFプログラムの実行時は管理者権限が必要になります。一般ユーザーで実行すると、ロード時にエラーになるようです。libelfの警告は、スケルトンヘッダーファイルを出力したときと同じものです。将来的に対応されるため、現状は無視して問題ありません。
次にトレースバッファーを表示します。
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
sh-1848885 [004] d... 372495.086860: bpf_trace_printk: Hi, execve!
byobu-status-1848887 [007] d... 372495.087401: bpf_trace_printk: Hi, execve!
byobu-status-1848888 [006] d... 372495.087782: bpf_trace_printk: Hi, execve!
byobu-status-1848890 [000] d... 372495.089048: bpf_trace_printk: Hi, execve!
(後略)
無事に、何かプロセスが実行されるたびに「Hi, execve!
」が表示されるようになりました。
終了はどちらもCtrl-Cで強制終了してください。今回は分量の都合で、コード部分はただトレースバッファーに単なる文字列を出力するだけのサンプルまでとなりますが、次回以降にeBPF maps等も活用したカスタマイズ方法を解説する予定です。
ここからは、先にMakefileやBCCのBPF CO-REバイナリ作成方法を紹介することにしましょう。
Makefileの作成
BPFオブジェクトを利用したプログラムは、複数のステップを踏むため再構築が若干面倒です。そこでmake
コマンド一発でビルドできるようにしておきましょう。
#!/usr/bin/make -f
APPS = execsnoop
CFLAGS += -g -O2 -Wall
LDFLAGS += $(shell pkg-config --libs libbpf)
ARCH ?= $(shell uname -m)
ARCH := $(patsubst x86_64,x86,$(ARCH))
ARCH := $(patsubst aarch64,arm64,$(ARCH))
ARCH := $(patsubst riscv64,riscv,$(ARCH))
.PHONY: all
all: $(APPS)
$(APPS): %: %.o %.skel.h
(CC) $(CFLAGS) $< $(LDFLAGS) -o $@
%.o: %.c %.skel.h
$(CC) $(CFLAGS) -c $< -o $@
%.skel.h: %.bpf.o
bpftool gen skeleton $< > $@
%.bpf.o: %.bpf.c vmlinux.h
clang $(CFLAGS) -target bpf -D__TARGET_ARCH_$(ARCH) -c $< -o $@
llvm-strip -g $@
vmlinux.h:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > $@
.PHONY: clean
clean:
-rm -f *.o *.skel.h vmlinux.h $(APPS)
.SECONDARY:
あまり複雑なことをはしていませんので、おおよそイメージできるかと思います。
__TARGET_ARCH_FOO
で使うARCH
変数はuanme -m
のそれとは異なるネーミングルールであるため、文字列置換を行っています。ここは愚直にpatsubst
関数を使っていますが、何かもう少しスマートな方法があるかもしれません。またMakefileにないアーキテクチャーは「make ARCH=名前
」で指定できます。
BPFオブジェクトを生成したあと、「 llvm-strip -g BPFオブジェクトファイル
」でデバッグ情報を削除しています。これは単にファイルサイズを小さくする措置です。実行しなくても問題ありません。
.SECONDARY:
は最終ターゲットに間接的にしか依存していない中間ファイル(たとえばmake all
時のexecsnoop.bpf.o
)を削除しないために追加しています。最終成果物だけあれば良いなら指定は不要です。
あとはmake
コマンドを実行するだけで、必要なものがすべて作られます。
$ make
rm -f *.o *.skel.h vmlinux.h execsnoop
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
clang -g -O2 -Wall -target bpf -D__TARGET_ARCH_x86 -c execsnoop.bpf.c -o execsnoop.bpf.o
llvm-strip -g execsnoop.bpf.o
bpftool gen skeleton execsnoop.bpf.o > execsnoop.skel.h
libbpf: elf: skipping unrecognized data section(4) .rodata.str1.1
cc -g -O2 -Wall -c execsnoop.c -o execsnoop.o
cc -g -O2 -Wall execsnoop.o -lbpf -o execsnoop
別のマシンにコピーして実行してみる
ビルド環境で動作確認をしたあとは、試しに別のマシンにバイナリだけコピーして実行してみましょう。
ubuntu@impishvm:~$ command -v clang
ubuntu@impishvm:~$ command -v gcc
ubuntu@impishvm:~$ ls /usr/include/bpf/
ls: cannot access '/usr/include/bpf/': No such file or directory
ubuntu@impishvm:~$ ldd execsnoop
linux-vdso.so.1 (0x00007ffda8c84000)
libbpf.so.0 => /lib/x86_64-linux-gnu/libbpf.so.0 (0x00007efea5139000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efea4f11000)
libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x00007efea4ef3000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007efea4ed7000)
/lib64/ld-linux-x86-64.so.2 (0x00007efea518c000)
ClangやGCC、libbpf-devパッケージはインストールされていない環境であっても、execsnoopの実行に必要なライブラリが揃っていることはわかります。では実際に、execsnoopを実行してみます。
# execsnoopの実行開始
ubuntu@impishvm:~$ ./execsnoop
libbpf: elf: skipping unrecognized data section(3) .rodata.str1.1
# 別の端末でトレースバッファーを表示
ubuntu@impishvm:~$ sudo cat /sys/kernel/debug/tracing/trace_pipe
lxd-agent-902 [000] d... 105.416197: bpf_trace_printk: Hi, execve!
su-904 [000] d... 105.443626: bpf_trace_printk: Hi, execve!
bash-905 [001] d... 105.445460: bpf_trace_printk: Hi, execve!
きちんと動くことがわかりましたね。他にもカーネルバージョンが違う環境でも試してみると良いでしょう。今回のサンプルコードは、カーネルの内部構造にはほとんどノータッチなため、特に問題なく動くはずです。
なおglibc等のバージョンが異なる場合は若干やっかいです。libbpfなら問題なく静的リンクできますが、glibcをきちんと動く形で静的リンクするのはそれなりに手間がかかります。glibc以外のlibcを用いてリンクしたり、libcの後方互換性に期待してターゲットの中で最も古い環境をビルド環境にするという方法などが必要になるでしょう。
setrlimit()
の必要性
eBPFのカーネル上のリソースは、Locked Memoryを利用していました。そのため、大きめのBPFプログラムをロードしようとすると、リソースが足りなくなります。そこでlibbpfを使うプログラムは、最初にsetrlimit()
でLocked Memoryのサイズを増やす ことが定番でした。
しかしながらKernel 5.11以降はMemory Cgroup(memcg)を使うように変更 されています。よってより新しいカーネルならsetrlimit()
の呼び出しは不要(なはず)です。今回のサンプルコードでは、Ubuntu 22.04 LTSで参照されることを想定してsetrlimit()
の呼び出しは行っていません。実行環境ごとの必要に応じて追加してください。もちろん、新しいカーネルであっても、あらかじめsetrlimit()
を呼び出しておくことは可能です。
VM版のLXDでBPF CO-REバイナリを動かすには
VM版のLXDで使われているlinux-kvm は、他のフレーバーに比べるとかなりカーネルコンフィグを絞った状態で提供されています。結果的にBPF CO-REバイナリやBCC/bpftraceのいくつかの機能が使えません。たとえば今回のサンプルも、SYSFSのトレーシング機能が無効化されているため、次のようなエラーメッセージが表示されます。
root@impishvm:~# ./execsnoop
libbpf: elf: skipping unrecognized data section(3) .rodata.str1.1
libbpf: failed to determine tracepoint 'syscalls/sys_enter_execve' perf event ID: No such file or directory
libbpf: prog 'syscalls__execve': failed to create tracepoint 'syscalls/sys_enter_execve' perf event: No such file or directory
libbpf: failed to auto-attach program 'syscalls__execve': -2
failed to attach BPF object
linux-kvmフレーバー向けに、BPFプログラム側で使用する機能を絞るか、次のようにgenericフレーバーに切り替えてしまいましょう。
$ sudo apt install -y linux-generic
$ sudo apt remove -y --purge '~nlinux-.*kvm ~i'
$ sudo reboot
2行目でKVMフレーバーを削除しているのは、genericフレーバーのみで起動するようにしたいためです。generic/KVMを併用する場合はそのまま残しても問題ありません。その場合、grub-rebootコマンドで設定するかGRUBメニューから手動で「Ubuntu, with Linux 5.13.0-21-generic
」と末尾に「generic」と書かれたメニューを選択してください。
なお、KVMカーネルで起動している状態でKVMカーネルを削除しようとすると、「 起動中のカーネルを削除しようとしているが、きちんと別のカーネルで起動してから削除することを強く推奨する。よって削除を中止して良いか? 」と聞かれます。削除したい場合は、きちんと「No 」を選びましょう。
ちなみに今のところ問題になりそうなのは、KVMフレーバーのみです。AWS・GCP・GKE・Azureフレーバーについてはざっと見た限り問題なさそうです。
BCCのlibbpf-toolsをビルドするには
BCCにはlibbpf版のツールが用意されています。実際にBPFプログラムを作る際にとても参考になるため、これをUbuntu 21.10上でコンパイルする方法も紹介しておきましょう。
まずは必要なパッケージをインストールし、ソースコードを取得しておきます。
$ sudo apt install build-essential libbpf-dev clang llvm linux-tools-common git make
$ git clone https://github.com/iovisor/bcc.git
$ cd bcc/libbpf-tools
ここでmakeしたいところではあるのですが、libbpf-toolsではlibbpfも自分でビルドする想定となっています。つまりgit clone
時に「--recursive
」オプションを付けるか、git clone
したあとに「git submodule init
」した上で「git submodule update
」する必要があります。もちろん自分でビルドしても良いのですが、ここはパッケージ版のlibbpfを流用するよう設定しておきます。
$ git diff
diff --git a/libbpf-tools/Makefile b/libbpf-tools/Makefile
index 5f7a9295..621a514b 100644
--- a/libbpf-tools/Makefile
+++ b/libbpf-tools/Makefile
@@ -4,7 +4,8 @@ CLANG ?= clang
LLVM_STRIP ?= llvm-strip
BPFTOOL ?= bin/bpftool
LIBBPF_SRC := $(abspath ../src/cc/libbpf/src)
-LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
+#LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
+LIBBPF_OBJ := /usr/lib/$(shell clang -print-multiarch)/libbpf.a
INCLUDES := -I$(OUTPUT) -I../src/cc/libbpf/include/uapi
CFLAGS := -g -O2 -Wall
INSTALL ?= install
あとは作りたいツールを単にmake
するだけです。
$ make execsnoop
MKDIR .output
BPF execsnoop.bpf.o
GEN-SKEL execsnoop.skel.h
CC execsnoop.o
CC trace_helpers.o
CC syscall_helpers.o
CC errno_helpers.o
CC map_helpers.o
CC uprobe_helpers.o
BINARY execsnoop
$ file execsnoop
execsnoop: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), (略)
$ ls -sh execsnoop
452K execsnoop
$ ldd execsnoop
linux-vdso.so.1 (0x00007fffab3dd000)
libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x00007f4d5e379000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f4d5e35d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4d5e135000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4d5e3fc000)
サイズがとても小さいにも関わらず、実際に実行してみるときちんと動作することがわかります。
$ sudo ./execsnoop
PCOMM PID PPID RET ARGS
sh 1683163 3517 0 /bin/sh -c byobu-status tmux_left
byobu-status 1683165 1683163 0 /usr/bin/byobu-status tmux_left
sh 1683164 3517 0 /bin/sh -c byobu-status tmux_right
byobu-status 1683166 1683164 0 /usr/bin/byobu-status tmux_right
ちなみにUbuntuのカーネルや各種パッケージのバージョンと、BCC側の状態に応じて、いくつかのコマンドはビルドできないかもしれません。またより新しいlibbpfを期待しているツールもあるかもしれませんので、どうしてもビルドできない場合はlibbpfのビルドから始めると良いでしょう。