Ubuntu Weekly Recipe

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

第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.hbpftoolコマンドで生成します。このvmlinux.hはカーネル内部の構造体定義を列挙したファイルで、BPFプログラムの中でincludeしています。これによりBPFプログラムはどの構造体にどのようにアクセスすれば良いか把握できます。

vmlinux.h自体はカーネルのバージョンに紐付いています。このためバージョンによっては構造体の中身が変わることがあります。しかしながらlibbpfがBPFオブジェクトをロードする際に、多少の変更点はよろしく対応してくれるのです[4]⁠。まずは上記のように実行中のカーネルから生成したデータを参照すると良いでしょう。

話をもとに戻すと、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-awsmain/l/linux-azuremain/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のビルドから始めると良いでしょう。

おすすめ記事

記事・ニュース一覧