eBPF第5章後半
5.6 CO-REのためのeBPFプログラムのコンパイル
コンパイルするときのMakefileの一部のオプションについて。
どうしてCO-REやlibbpfのプログラムに以下のオプションが必要なのかを解説する。
デバッグ情報を取り除く
デバッグ情報は以下のコマンドで取り除きます。
llvm-strip -g <object file>
llvm-stripの簡単な使い方の解説がこちら:
最適化
-O2オプションをつけないと callx <register>
というアセンブリコードを出力するが、eBPFはレジスタからのアドレスの呼び出しをサポートしていない。
でもサポートがそのうちされる?っぽいイシューを見つけました
ターゲットアーキテクチャ
libbpfではコンパイル時にターゲットアーキテクチャを指定する必要があり、kprobeなどのプラットフォーム固有マクロがいくつか定義されている。
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
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での定義内容から探しアタッチポイントを自動的に取得する。
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 の定義はこちら。
pollをして、データが到着したとき、バッファが満杯になったときに前述のコールバック関数らが呼ばれる。
最後にリングバッファを解放して、プログラムとMapを破棄する。
perf_buffer__free(pb);
hello_buffer_config_bpf__destroy(skel); // clean up
5.9.2 libbpfサンプルコード
参考になりそうな資料
libbpf-tools
5.10 まとめ
- CO-REによってeBPFプログラムの移植性が大幅に向上する。
- CO-REでは型情報をコンパイル済みオブジェクトファイルに埋め込み、カーネルへのロード時に命令を書き換える再配置をする
- カーネルで実行するeBPFプログラムを書いた
- スケルトンを使ってユーザ空間でライフサイクルを管理してカーネルで走らせるeBPFプログラムを書いた
次は演習:https://zenn.dev/greenteabiscuit/articles/6c734c2085829b
Discussion