Ubuntu Weekly Recipe

第694回 libbpfとclangでポータブルなBPF CO-REバイナリ作成

この記事を読むのに必要な時間:およそ 10.5 分

実際に実行してみる

今回のコードは,単にトレースバッファーに書き込んでいるだけです。よって実行しても何も表示されません。結果を確認したい場合は,別の端末からトレースバッファーを表示しましょう。まずは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の後方互換性に期待してターゲットの中で最も古い環境をビルド環境にするという方法などが必要になるでしょう。

著者プロフィール

柴田充也(しばたみつや)

Ubuntu Japanese Team Member株式会社 創夢所属。数年前にLaunchpad上でStellariumの翻訳をしたことがきっかけで,Ubuntuの翻訳にも関わるようになりました。