Ubuntu Weekly Recipe

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

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

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⁠。

※2
残念ながらlibbpfにはまだ網羅的なAPIドキュメントがありません。このため何かやりたい場合は,libbpfのコードやBCC/libbpf-tools以下のコードを参照しながら記述することになります。

最後の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⁠。

※3
BPFプログラムやそれを利用するユーザーランドプログラムのライセンスに関してはカーネルドキュメントのBPF licensingも参照してください。

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⁠。まずは上記のように実行中のカーネルから生成したデータを参照すると良いでしょう。

※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オブジェクト側の準備が整いました。

著者プロフィール

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

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