第688回と第690回では、
今回は一般的なコンパイラのようにバイナリを生成でき、
BPF CO-REの登場
BPF CO-RE
- BPFを利用したバイナリを実行環境とは異なる環境でビルドできる
- カーネルバージョン間の違いもある程度は吸収してくれる
- 実行バイナリのサイズはそれなりに小さくなる
- コンテナ内部などターゲットと異なるカーネルが動いている環境でもビルドできる
つまりステージング環境や開発環境でビルド&テストしたバイナリを、
実はBCCでは、
ただしBPF CO-REを使用するためには、
Ubuntuの場合、
たとえばUbuntu 21.
$ 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.
今回はUbuntu 21.
BPF CO-REのコンパイル環境の準備
今回も第690回と同じように、execve()
をトレースするコードを題材として使いましょう。ちなみに第692回ではsysfs以下を使ってツールレスに同等の機能を実現したり、
BPF CO-REのバイナリをコンパイルするには、
bpftool
コマンドでターゲットとなるカーネルバージョンのvmlinux.
を入手するh - ターゲットのカーネルにロードされるBPFプログラムをClangでコンパイルしてBPFオブジェクトファイルを生成する
- そのBPFオブジェクトファイルから、
bpftool
コマンドを用いてユーザーランドのプログラムで使うためのスケルトンヘッダーファイルを生成する - スケルトンヘッダーファイルを使って、
ユーザーランドのプログラムをコンパイルする
簡単に言うと、
まずはビルドに必要なツール一式をインストールしておきましょう。
$ sudo apt install build-essential libbpf-dev clang llvm linux-tools-generic
前半はコンパイルに必要なツール群です。最後のlinux-tools-genericパッケージはビルドに使うbpftool
コマンドと、bpftool
コマンドは、bpftool
のカーネルバージョンに依存しないサブコマンドだけを使うなら、
今回は最低限のコードのみを紹介します。必要なのは
BPFプログラムのコード
まずはBPFプログラムのコードexecsnoop.
)
/* 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バイナリの中の指定したセクションに配置だけのマクロで、__
」
最初のtracepoint/
セクションには、
- tracepoint:
/sys/
以下にあるイベント名をもとにkernel/ debug/ tracing/ events/ 「 tracepoint/イベント名
」「 tp/イベント名
」 - kprobe、
kretprobe: 「 kprobe/関数名
」
こんな感じで設定していきます。今回はsyscalls/
イベントなのでtracepoint/
」
関数名のほうは任意の名前が使えます。今回は第690回の例に合わせてみました。関数の中でやっていることはbpf_
」bpf_
」
最後のlicense
セクションは、
これはカーネル内部にロードされるBPFオブジェクトであっても同じで、
cannot call GPL-restricted function from non-GPL compatible program
ここでは3条項BSDとGPL 2.
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.
をbpftool
コマンドで生成します。このvmlinux.
はカーネル内部の構造体定義を列挙したファイルで、
vmlinux.
自体はカーネルのバージョンに紐付いています。このためバージョンによっては構造体の中身が変わることがあります。しかしながらlibbpfがBPFオブジェクトをロードする際に、
話をもとに戻すと、vmlinux.
を生成したら次はclang
コマンドによるコンパイルでBPFオブジェクトを生成します。-target bpf
」-D__
」
生成された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オブジェクトのファイル名__シンボル名
」execsnoop.
」execsnoop.
」open
」execsnoop_
」struct execsnoop_
」bpftool gen skeleton execsnoop.
」name 名前
」
スケルトンヘッダーファイルを生成時に次のような警告メッセージが表示されいてます。
libbpf: elf: skipping unrecognized data section(4) .rodata.str1.1
これはlibbpfが古いことによるもので、
ここまででBPFオブジェクト側の準備が整いました。
異なるカーネルのvmlinux.h
を生成するには
bpftool
プログラムはvmlinux
ファイルからもvmlinux.
を生成してくれます。もしamd64マシンの上から異なるアーキテクチャーのvmlnux.
を生成したいなら次のようにデバッグシンボル付きパッケージを取得・
$ 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
デバッグシンボル付きパッケージは、
もし実機があるなら次の方法でダウンロード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' (略)
ここの
ユーザーランドプログラムの作成
次に、execsnoop.
)
/* 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_
」
FOO__
でBPFオブジェクトをメモリ上に展開します。一度成功したあとは、open() 不要になったら FOO__
で破棄する必要があります。destroy() FOO__
でBPFオブジェクトの検証が行われます。コードに問題があった場合は、load() だいたいはここでエラーになります。 FOO__
でカーネルにアタッチします。実際にBPFプログラムが動き出すのはこのタイミングです。attach()
FOO__
とFOO__
はまとめて実行するFOO__
も用意されているので、
今回は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をリンクしていますが、
実際に実行してみる
今回のコードは、execsnoop
を実行します。
$ sudo ./execsnoop libbpf: elf: skipping unrecognized data section(4) .rodata.str1.1
BPFプログラムの実行時は管理者権限が必要になります。一般ユーザーで実行すると、
次にトレースバッファーを表示します。
$ 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で強制終了してください。今回は分量の都合で、
ここからは、
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:
あまり複雑なことをはしていませんので、
__
で使うARCH
変数はuanme -m
のそれとは異なるネーミングルールであるため、patsubst
関数を使っていますが、make ARCH=名前
」
BPFオブジェクトを生成したあと、llvm-strip -g BPFオブジェクトファイル
」
.SECONDARY:
は最終ターゲットに間接的にしか依存していない中間ファイルmake all
時のexecsnoop.
)
あとは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、
# 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なら問題なく静的リンクできますが、
setrlimit()
の必要性
eBPFのカーネル上のリソースは、setrlimit()
でLocked Memoryのサイズを増やすことが定番でした。
しかしながらKernel 5.setrlimit()
の呼び出しは不要setrlimit()
の呼び出しは行っていません。実行環境ごとの必要に応じて追加してください。もちろん、setrlimit()
を呼び出しておくことは可能です。
VM版のLXDでBPF CO-REバイナリを動かすには
VM版のLXDで使われているlinux-kvmは、
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フレーバー向けに、
$ sudo apt install -y linux-generic $ sudo apt remove -y --purge '~nlinux-.*kvm ~i' $ sudo reboot
2行目でKVMフレーバーを削除しているのは、Ubuntu, with Linux 5.
」
なお、
ちなみに今のところ問題になりそうなのは、
BCCのlibbpf-toolsをビルドするには
BCCにはlibbpf版のツールが用意されています。実際にBPFプログラムを作る際にとても参考になるため、
まずは必要なパッケージをインストールし、
$ 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したいところではあるのですが、git clone
時に--recursive
」git clone
したあとにgit submodule init
」git submodule update
」
$ 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のカーネルや各種パッケージのバージョンと、