そろそろBPFのことを思い出そうと思って記事を書く。
シリーズです:
前回まででRustでのBPF CO-REバイナリの作成方法をまとめたが、C言語などで使う場合の細かい手順を追ってみる。
ちゃんとドキュメントを追い切れていないところがあり、今回は、iovisor/bccのlibbpf-toolsにあるMakefileなどを参照したことは了解願いたい。
さて、利用するBPFプログラムは以下のようにする。 vfs_read
をトレースして、読み込みに成功いたバイト数を、log2をbinとするヒストグラムにする。
#include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #include <bpf/bpf_core_read.h> #define MAX_ENTRIES 10240 #define MAX_SLOTS 15 // https://github.com/iovisor/bcc/blob/master/libbpf-tools/bits.bpf.h static __always_inline u64 log2(u32 v) { u32 shift, r; r = (v > 0xFFFF) << 4; v >>= r; shift = (v > 0xFF) << 3; v >>= shift; r |= shift; shift = (v > 0xF) << 2; v >>= shift; r |= shift; shift = (v > 0x3) << 1; v >>= shift; r |= shift; r |= (v >> 1); return r; } static __always_inline u64 log2l(u64 v) { u32 hi = v >> 32; if (hi) return log2(hi) + 32; else return log2(v); } struct hist { __u32 slots[MAX_SLOTS]; }; const volatile pid_t targ_pid = -1; static struct hist initial_hist = {0}; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, MAX_ENTRIES); __type(key, u32); // pid __type(value, struct hist); // slots __uint(map_flags, BPF_F_NO_PREALLOC); } hists SEC(".maps"); SEC("kretprobe/vfs_read") int BPF_KRETPROBE(vfs_read, ssize_t ret) { if (ret < 0) goto cleanup; u32 pid; u64 slot; struct hist *histp; pid = bpf_get_current_pid_tgid(); if(targ_pid != -1 && pid != (u32)targ_pid) goto cleanup; histp = bpf_map_lookup_elem(&hists, &pid); if (!histp) { bpf_map_update_elem(&hists, &pid, &initial_hist, 0); histp = bpf_map_lookup_elem(&hists, &pid); if (!histp) goto cleanup; } slot = log2l(ret); if (slot >= MAX_SLOTS) slot = MAX_SLOTS - 1; __sync_fetch_and_add(&histp->slots[slot], 1); cleanup: return 0; } char LICENSE[] SEC("license") = "GPL";
このプログラムを以下のようにビルドする。
# ビルドに必要な vmlinux.h は bpftool btf dump で生成。 # ref: https://facebookmicrosites.github.io/bpf/blog/2020/02/19/bpf-portability-and-co-re.html#btf $ bpftool btf dump file /sys/kernel/btf/vmlinux format c > bpf/vmlinux.h $ clang -g -O2 -target bpf \ -D__TARGET_ARCH_$(uname -m | sed 's/x86_64/x86/') \ -c bpf/helloworld.bpf.c -o bpf/helloworld.bpf.o
この helloworld.bpf.o
はELF形式のバイナリで、色々とメタデータが付与されている。セクションヘッダを眺めると、 .rodata
や .maps
のほか、BTFに関する情報が格納されている。
$ readelf -S bpf/helloworld.bpf.o There are 24 section headers, starting at offset 0x30b0: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .strtab STRTAB 0000000000000000 00002f45 0000000000000164 0000000000000000 0 0 1 [ 2] .text PROGBITS 0000000000000000 00000040 0000000000000000 0000000000000000 AX 0 0 4 [ 3] kretprobe/vf[...] PROGBITS 0000000000000000 00000040 0000000000000338 0000000000000000 AX 0 0 8 [ 4] .relkretprob[...] REL 0000000000000000 000024f0 0000000000000050 0000000000000010 23 3 8 [ 5] .rodata PROGBITS 0000000000000000 00000378 0000000000000004 0000000000000000 A 0 0 4 [ 6] license PROGBITS 0000000000000000 0000037c 0000000000000004 0000000000000000 WA 0 0 1 [ 7] .maps PROGBITS 0000000000000000 00000380 0000000000000028 0000000000000000 WA 0 0 8 [ 8] .bss NOBITS 0000000000000000 000003a8 000000000000003c 0000000000000000 WA 0 0 4 [ 9] .debug_loc PROGBITS 0000000000000000 000003a8 000000000000029b 0000000000000000 0 0 1 [10] .debug_abbrev PROGBITS 0000000000000000 00000643 0000000000000197 0000000000000000 0 0 1 [11] .debug_info PROGBITS 0000000000000000 000007da 00000000000004d6 0000000000000000 0 0 1 [12] .rel.debug_info REL 0000000000000000 00002540 0000000000000610 0000000000000010 23 11 8 [13] .debug_str PROGBITS 0000000000000000 00000cb0 000000000000024f 0000000000000001 MS 0 0 1 [14] .BTF PROGBITS 0000000000000000 00000eff 00000000000007a5 0000000000000000 0 0 1 [15] .rel.BTF REL 0000000000000000 00002b50 0000000000000040 0000000000000010 23 14 8 [16] .BTF.ext PROGBITS 0000000000000000 000016a4 00000000000003bc 0000000000000000 0 0 1 [17] .rel.BTF.ext REL 0000000000000000 00002b90 0000000000000380 0000000000000010 23 16 8 [18] .debug_frame PROGBITS 0000000000000000 00001a60 0000000000000028 0000000000000000 0 0 8 [19] .rel.debug_frame REL 0000000000000000 00002f10 0000000000000020 0000000000000010 23 18 8 [20] .debug_line PROGBITS 0000000000000000 00001a88 0000000000000168 0000000000000000 0 0 1 [21] .rel.debug_line REL 0000000000000000 00002f30 0000000000000010 0000000000000010 23 20 8 [22] .llvm_addrsig LOOS+0xfff4c03 0000000000000000 00002f40 0000000000000005 0000000000000000 E 23 0 1 [23] .symtab SYMTAB 0000000000000000 00001bf0 0000000000000900 0000000000000018 1 92 8
これらセクションヘッダの情報などから、ユーザランド側で使うヘッダを生成する。
bpftool gen skeleton bpf/helloworld.bpf.o > src/helloworld.bpf.h
helloworld.bpf.h
はこういう内容になっている。
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ /* THIS FILE IS AUTOGENERATED! */ #ifndef __HELLOWORLD_BPF_SKEL_H__ #define __HELLOWORLD_BPF_SKEL_H__ #include <stdlib.h> #include <bpf/libbpf.h> // セクション情報などからBPFプログラムを表現する // 構造体を生成する struct helloworld_bpf { struct bpf_object_skeleton *skeleton; struct bpf_object *obj; struct { struct bpf_map *hists; struct bpf_map *rodata; struct bpf_map *bss; } maps; struct { struct bpf_program *vfs_read; } progs; struct { struct bpf_link *vfs_read; } links; struct helloworld_bpf__bss { struct hist initial_hist; } *bss; struct helloworld_bpf__rodata { pid_t targ_pid; } *rodata; }; // 以下、helloworld_bpf構造体を操作し、BPFプログラムをアタッチするための // ラッパ関数が並ぶ。 static void helloworld_bpf__destroy(struct helloworld_bpf *obj) { if (!obj) return; if (obj->skeleton) bpf_object__destroy_skeleton(obj->skeleton); free(obj); } // ... static inline int helloworld_bpf__create_skeleton(struct helloworld_bpf *obj) { struct bpf_object_skeleton *s; s = (struct bpf_object_skeleton *)calloc(1, sizeof(*s)); if (!s) return -1; obj->skeleton = s; // ... // 最後に、BPFプログラム自体がスタティックなデータとして // ヘッダに埋め込まれる。 s->data_sz = 14000; s->data = (void *)"\ \x7f\x45\x4c\x46\x02\x01\x01\0\0\0\0\0\0\0\0\0\x01\0\xf7\0\x01\0\0\0\0\0\0\0\0\ \0\0\0\0\0\0\0\0\0\0\0\xb0\x30\0\0\0\0\0\0\0\0\0\0\x40\0\0\0\0\0\x40\0\x18\0\ \x01\0\x79\x17\x50\0\0\0\0\0\xb7\x01\0\0\0\0\0\0\x6d\x71\x62\0\0\0\0\0\x85\0\0\ ...\ \0\0\x09\0\0\0\0\0\0\x01\0\0\0\x5c\0\0\0\x08\0\0\0\0\0\0\0\x18\0\0\0\0\0\0\0"; return 0; err: bpf_object__destroy_skeleton(s); return -1; } #endif /* __HELLOWORLD_BPF_SKEL_H__ */
最後にこのヘッダを利用したCのプログラムを書けば、BPF CO-REなバイナリができる。
// BPFプログラムと共通のレイアウトの構造体 #include <asm/types.h> #define MAX_SLOTS 15 struct hist { __u32 slots[MAX_SLOTS]; }; #include "helloworld.bpf.h" #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <bpf/libbpf.h> #include <bpf/bpf.h> // libbpf処理内部のログを出すための関数。 int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { if (level == LIBBPF_DEBUG) return 0; return vfprintf(stderr, format, args); } int main(int argc, char **argv) { libbpf_set_print(libbpf_print_fn); int err, i; struct helloworld_bpf *obj = helloworld_bpf__open(); if (!obj) { fprintf(stderr, "failed to open BPF object\n"); exit(1); } if(argc > 1) obj->rodata->targ_pid = (pid_t)strtol(argv[1], NULL, 10); err = helloworld_bpf__load(obj); if (err) { fprintf(stderr, "failed to load BPF programs\n"); goto cleanup; } err = helloworld_bpf__attach(obj); if (err) { fprintf(stderr, "failed to attach BPF programs\n"); goto cleanup; } printf("Tracing vfs_read in 3 secs.\n"); for (i = 0; i < 3; i++) { printf(".\n"); sleep(1); } printf("----\n"); { struct bpf_map *hists = obj->maps.hists; int fd = bpf_map__fd(hists); __u32 lookup_key = -2, next_key; struct hist h; while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) { err = bpf_map_lookup_elem(fd, &next_key, &h); if (err < 0) { fprintf(stderr, "failed to lookup hist: %d\n", err); return -1; } printf("pid: %d\n", next_key); for(i = 0; i < MAX_SLOTS; i++) printf("\tvalue: hist[%d] = %d\n", i, h.slots[i]); lookup_key = next_key; } } cleanup: helloworld_bpf__destroy(obj); return err != 0; }
ビルドはただのCプログラムと同様。
$ gcc src/hello.c -o hello.out -l bpf $ ls -lh hello.out -rwxr-xr-x 1 vagrant vagrant 30K Mar 15 11:08 hello.out
実験してみる。以下のように偏った値のreadをプログラムから発行させる。
$ ruby -e 'puts $$; f = open("/dev/urandom"); loop { f.read([2 << 3, 2 << 6, 2 << 10].sample) }' 239576
このPIDを先程のツールにトレースさせると、確かに偏ったバイト数が連続でreadされ続けているらしいとわかる。
$ sudo ./hello.out 239576 Tracing vfs_read in 3 secs. . . . ---- pid: 239576 value: hist[0] = 0 value: hist[1] = 0 value: hist[2] = 0 value: hist[3] = 0 value: hist[4] = 196622 value: hist[5] = 0 value: hist[6] = 0 value: hist[7] = 196582 value: hist[8] = 0 value: hist[9] = 0 value: hist[10] = 0 value: hist[11] = 197303 value: hist[12] = 0 value: hist[13] = 0 value: hist[14] = 0
C言語でのBPF CO-REバイナリのビルド手順を、なるべく細かく記録した。
このヘッダを利用すればたとえば mrubyでもBPF CO-REのワンバイナリツールが作れる と思うので、RbBCCの夢が再びじゃんという感じなのだが、いかんせんlibbpfをmrubyから丁寧にラップするところからなので、大変そう...。今度頑張ろうと思います。