😊

eBPF第5章後半

2024/07/25に公開

5.6 CO-REのためのeBPFプログラムのコンパイル

コンパイルするときのMakefileの一部のオプションについて。

どうしてCO-REやlibbpfのプログラムに以下のオプションが必要なのかを解説する。

デバッグ情報を取り除く

デバッグ情報は以下のコマンドで取り除きます。

llvm-strip -g <object file>

llvm-stripの簡単な使い方の解説がこちら:

https://qiita.com/termoshtt/items/0ec332018ee7087ba820

最適化

-O2オプションをつけないと callx <register> というアセンブリコードを出力するが、eBPFはレジスタからのアドレスの呼び出しをサポートしていない。

でもサポートがそのうちされる?っぽいイシューを見つけました

https://github.com/microsoft/ebpf-for-windows/issues/3237

ターゲットアーキテクチャ

libbpfではコンパイル時にターゲットアーキテクチャを指定する必要があり、kprobeなどのプラットフォーム固有マクロがいくつか定義されている。

https://github.com/torvalds/linux/blob/master/tools/lib/bpf/bpf_tracing.h#L799C1-L821C40

kprobeの引数にはCPUレジスタの内容のコピーを保持するpt_regs構造体があり、実行しているアーキテクチャに依存する。

pt_regsのStructは2種類あるのを見つけた。

struct pt_regs___arm64 {
	unsigned long orig_x0;
};

...

struct pt_regs___s390 {
	unsigned long orig_gpr2;
};

Makefile

オブジェクトをコンパイルするためのMakefileの説明。
こちらのオブジェクトファイルのBTF情報を読んでいく。

オブジェクトファイル内のBTF情報

BTF情報は.BTFと.BTF.extの二つのセクションに格納される。
.BTFはデータと文字列情報、.BTF.extは関数とコードの行情報を格納する。

$ readelf -S hello-buffer-config.bpf.o | grep BTF
  [ 8] .BTF              PROGBITS         0000000000000000  000003b0
  [ 9] .rel.BTF          REL              0000000000000000  00000ec8
  [10] .BTF.ext          PROGBITS         0000000000000000  00000ac0
  [11] .rel.BTF.ext      REL              0000000000000000  00000f18

サイズがだいたいわかる?
// Size of .BTF = 00000ec8 - 000003b0 = 0b18 (hex) = 2840 (dec) bytes

https://www.kernel.org/doc/html/latest/bpf/btf.html#elf-file-format-interface

5.7 BPFの再配置

再配置がどのような仕組みで行われているか解説。

libbpfはeBPFプログラムの内容を実行時に変更してターゲットマシンのカーネルで動作させるためにCO-RE再配置情報を使う。

再配置はこのStructを使う

struct bpf_core_relo {
	__u32 insn_off; // 命令
	__u32 type_id; // 構造体の型
	__u32 access_str_off; // オフセット
	enum bpf_core_relo_kind kind;
};

bpf.hに例がのってある

 * Example:
 *   struct sample {
 *       int a;
 *       struct {
 *           int b[10];
 *       };
 *   };
 *
 *   struct sample *s = ...;
 *   int *x = &s->a;     // encoded as "0:0" (a is field #0)
 *   int *y = &s->b[5];  // encoded as "0:1:0:5" (anon struct is field #1,
 *                       // b is field #0 inside anon struct, accessing elem #5)
 *   int *z = &s[10]->b; // encoded as "10:1" (ptr is used as an array)

BPFプログラムがロードされると、再配置情報とBTF情報が読み込まれる。

オフセットXにある命令は、sampleをもとに再配置されないといけないと再配置情報が教えてくれる。

ローダはBTFを使って変数a、bのオフセットを取得して命令を再配置する。

感じの流れだそうですが、よくわからない。。。

自分のログからID 22 が pt_regsを参照しており、vmlinux内に存在する型83を持つpt_regsに相当すると判断している。

libbpf: CO-RE relocating [22] struct pt_regs: found target candidate [83] struct pt_regs in [vmlinux]
libbpf: prog 'hello': relo #0: <byte_off> [22] struct pt_regs.di (0:14 @ offset 112)
libbpf: prog 'hello': relo #0: matching candidate #0 <byte_off> [83] struct pt_regs.di (0:14 @ offset 112)
libbpf: prog 'hello': relo #0: patched insn #5 (ALU/ALU64) imm 112 -> 112
libbpf: prog 'hello': relo #1: <byte_off> [22] struct pt_regs.di (0:14 @ offset 112)
libbpf: prog 'hello': relo #1: matching candidate #0 <byte_off> [83] struct pt_regs.di (0:14 @ offset 112)
libbpf: prog 'hello': relo #1: patched insn #6 (LDX/ST/STX) off 112 -> 112
libbpf: prog 'hello': relo #2: <byte_off> [22] struct pt_regs.di (0:14 @ offset 112)
libbpf: prog 'hello': relo #2: matching candidate #0 <byte_off> [83] struct pt_regs.di (0:14 @ offset 112)
libbpf: prog 'hello': relo #2: patched insn #48 (LDX/ST/STX) off 112 -> 112

5.8 CO-REユーザ空間コード

bpftoolをエンドユーザーに毎回叩かせるのはエンドユーザーにとって面倒。
ユーザ空間側で使えるラッパーコードがあれば便利ということで、ここではユーザ空間で利用できるlibbpfライブラリについて話す。

5.9 ユーザ空間のlibbpfライブラリ

libbpfライブラリはユーザ空間でアプリケーションを作ることを可能にする。システムコールをラップする関数を提供しており、

  • カーネルへのプログラムのロード
  • イベントへのアタッチ
  • Map情報のアクセス

などができる。

5.9.1 BPFスケルトン

スケルトンを使ってユーザランドからeBPFプログラムとMapのライフサイクルを管理することができる。

int main()
{
    int err;
	struct perf_buffer *pb = NULL;

	libbpf_set_print(libbpf_print_fn); // libbpfが生成するログメッセージを出力する際に呼び出すコールバック関数を設定する


	skel = hello_buffer_config_bpf__open_and_load(); // skel構造体を作り、カーネルにロードする
	if (!skel) {
		printf("Failed to open BPF object\n");
		return 1;
	}

	err = hello_buffer_config_bpf__attach(skel); // プログラムをアタッチする
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
		hello_buffer_config_bpf__destroy(skel);
        return 1;
	}

	pb = perf_buffer__new(bpf_map__fd(skel->maps.output), 8, handle_event, lost_event, NULL, NULL); // Perfリングバッファ出力を処理するための構造体を作成

...

	while (true) {
		err = perf_buffer__poll(pb, 100 /* timeout, ms */); // polling

...

	perf_buffer__free(pb);
	hello_buffer_config_bpf__destroy(skel); // clean up

5.9.1.1 プログラムとMapをカーネルにロード

  • open

ELFデータを読み出し、各セクションをそれぞれの構造体に変換

  • load

カーネルにロード。スケルトンはユーザ空間におけるELFデータの情報を表現したものなので、カーネルにロードされたあとにスケルトンを変更しても何も起こらず、意味はない。

以下のコードはopen, load両方をやっているが、それぞれの関数も存在する。

skel = hello_buffer_config_bpf__open_and_load();

5.9.1.2 既存のMapへのアクセス

Mapはピン留めすることができ、ピン留めされたパスを知っていればbpf_obj_getを使って既存のMapへのファイル記述子を取得できる。

$ sudo bpftool map create /sys/fs/bpf/findme type array key 4 value 32 entries 4 name findme

$ sudo ./find-map
name findme

5.9.1.3 イベントへのアタッチ

こちらでプログラムをシステムコール関数にアタッチすることができる。

err = hello_buffer_config_bpf__attach(skel); // プログラムをアタッチする

SECでの定義内容から探しアタッチポイントを自動的に取得する。

https://github.com/torvalds/linux/blob/master/tools/lib/bpf/libbpf.c

struct bpf_link *bpf_program__attach_kprobe(const struct bpf_program *prog,
					    bool retprobe,
					    const char *func_name)
{
	DECLARE_LIBBPF_OPTS(bpf_kprobe_opts, opts,
		.retprobe = retprobe,
	);

	return bpf_program__attach_kprobe_opts(prog, func_name, &opts);
}

...

struct bpf_link *bpf_program__attach_xdp(const struct bpf_program *prog, int ifindex)
{
	/* target_fd/target_ifindex use the same field in LINK_CREATE */
	return bpf_program_attach_fd(prog, ifindex, "xdp", NULL);
}

5.9.1.4 イベントバッファの管理

Perfリングバッファあはlibbpfのものを使う

	pb = perf_buffer__new(bpf_map__fd(skel->maps.output), 8, handle_event, lost_event, NULL, NULL);

libbpf の定義はこちら。

https://github.com/torvalds/linux/blob/master/tools/lib/bpf/libbpf.c#L13189C1-L13222C2

pollをして、データが到着したとき、バッファが満杯になったときに前述のコールバック関数らが呼ばれる。

https://github.com/torvalds/linux/blob/master/tools/lib/bpf/libbpf.c#L13460C1-L13478C2

最後にリングバッファを解放して、プログラムとMapを破棄する。

	perf_buffer__free(pb);
	hello_buffer_config_bpf__destroy(skel); // clean up

5.9.2 libbpfサンプルコード

参考になりそうな資料

https://github.com/libbpf/libbpf-bootstrap

libbpf-tools

https://github.com/iovisor/bcc/tree/master/libbpf-tools

5.10 まとめ

  • CO-REによってeBPFプログラムの移植性が大幅に向上する。
  • CO-REでは型情報をコンパイル済みオブジェクトファイルに埋め込み、カーネルへのロード時に命令を書き換える再配置をする
  • カーネルで実行するeBPFプログラムを書いた
  • スケルトンを使ってユーザ空間でライフサイクルを管理してカーネルで走らせるeBPFプログラムを書いた

次は演習:https://zenn.dev/greenteabiscuit/articles/6c734c2085829b

Discussion