執筆者:稲葉貴昭
1. はじめに
前回の概論編では、eBPF(以下、BPFと表記)がどんなものでどのように実現されているかを中心に解説いたしました。 また、BCCやBCC-Tools、CO-REについての解説も行いました。 実装編となる今回は、BCC-Toolsの後継となるlibbpf-toolsよりopensnoopコマンドに着目して ソースコードレベルの解説を行います。
2. 使用環境
BPFは、2022年8月現在も活発に開発が進められているため、理由がなければ新しいカーネルバージョンを使ったほうがいいです。
特にCO-REについては、libbpfのバージョンの関係でUbuntuだと20.10以降にする必要があります。
今回の調査は、最新のLTS版であるUbuntu 22.04 LTSを使用しました。
調査を行ったのは、執筆時点で最新のバージョンだったBCC v0.24.0です。*1
libbpf-toolsのリポジトリを見ると*.bpf.cと*.cというファイルが大量にあることがわかります。
ファイル名が「*.bpf.c」となっているのは、前回の記事におけるBPFプログラムに当たります。
また、ファイル名が「*.c」になっているのは、前回の記事におけるBPFアプリケーションになります。
さて、本記事ではこれらコマンドの中からopensnoopというコマンドの解説を軸にします。 ただし、すべてのコードについて解説をいれると相当な量になるため、筆者がポイントになっていると感じた箇所のみ解説する形にいたします。
3. コンパイル
以下の手順でcloneとコンパイルができます。
sudo apt install make gcc libelf-dev clang llvm git clone https://github.com/iovisor/bcc.git cd bcc git checkout -b v0.24.0 refs/tags/v0.24.0 git submodule update --init --recursive cd libbpf-tools make
4. opensnoopとは
opensnoopは、openシステムコール
が呼ばれた際に、どのプロセスがどのファイルをopenしたのかをトレースするためのコマンドです。
usageは以下のようになっています。
Usage: opensnoop [OPTION...] Trace open family syscalls USAGE: opensnoop [-h] [-T] [-U] [-x] [-p PID] [-t TID] [-u UID] [-d DURATION] [-n NAME] [-e] EXAMPLES: ./opensnoop # trace all open() syscalls ./opensnoop -T # include timestamps ./opensnoop -U # include UID ./opensnoop -x # only show failed opens ./opensnoop -p 181 # only trace PID 181 ./opensnoop -t 123 # only trace TID 123 ./opensnoop -u 1000 # only trace UID 1000 ./opensnoop -d 10 # trace for 10 seconds only ./opensnoop -n main # only print process names containing "main" ./opensnoop -e # show extended fields -d, --duration=DURATION Duration to trace -e, --extended-fields Print extended fields -n, --name=NAME Trace process names containing this -p, --pid=PID Process ID to trace -t, --tid=TID Thread ID to trace -T, --timestamp Print timestamp -u, --uid=UID User ID to trace -U, --print-uid Print UID -v, --verbose Verbose debug output -x, --failed Failed opens only -?, --help Give this help list --usage Give a short usage message -V, --version Print program version
以下はオプション指定なしで実行したときの出力結果です。
実行には管理者権限が必要になります。
左から、「openを呼んだプロセスID」、「プロセス名」、「openの戻り値であるファイルディスクリプタ」、
「errno」、「openしたファイルパス」となっています。
PID COMM FD ERR PATH 2870 systemd-journal 39 0 /proc/618498/comm 2870 systemd-journal 39 0 /proc/618498/cmdline ... 618500 opensnoop 24 0 /etc/localtime 2870 systemd-journal 39 0 /proc/618498/loginuid 2870 systemd-journal 39 0 /proc/618498/cgroup ... 67345 tmux: server 9 0 /proc/618498/cmdline 24927 irqbalance 6 0 /proc/interrupts
5. opensnoop.bpf.cの解説
opensnoop.bpf.cは、opensnoopにおけるBPFプログラムにあたる部分です。
5.1 mapの宣言
10 const volatile __u64 min_us = 0; 11 const volatile pid_t targ_pid = 0; 12 const volatile pid_t targ_tgid = 0; 13 const volatile uid_t targ_uid = 0; 14 const volatile bool targ_failed = false; 15 16 struct { 17 __uint(type, BPF_MAP_TYPE_HASH); 18 __uint(max_entries, 10240); 19 __type(key, u32); 20 __type(value, struct args_t); 21 } start SEC(".maps"); 22 23 struct { 24 __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); 25 __uint(key_size, sizeof(u32)); 26 __uint(value_size, sizeof(u32)); 27 } events SEC(".maps");
ここでは、mapの宣言を行っているのですが、BPF特有のC言語の書き方になっています。
まず、先頭4行の変数ですが、これらはopensnoopの-p/-t/-u/-fオプションを指定するときに使用されるものです。
例えば、-pオプションは引数で指定したプロセスIDがopenシステムコールを呼んだ時のみ出力するオプションなので、
BPFアプリケーションの引数解析処理が終わった後で引数指定したプロセスIDがセットされます。
C言語を知っている人からすれば、「constで宣言しているのに後で書き換える?」、「コンパイル単位が違うプログラムで
値を書き換える?」と思われるかもしれません。
この辺の疑問はBPFアプリケーションの解説時に後述するので、ここでは「constで宣言しているからrodataセクションに配置」
されるということだけご認識ください。
下の2つの構造体は、mapの宣言になります。
startは、プロセスIDをkeyに、openシステムコールに渡しているfilename、flagsをvalueにした最大エントリ数10240のhash mapで、
引数を一時的に格納するために使用されます。
eventsは、BPFプログラムの処理結果をBPFアプリケーション側と共有するためのものです。
SEC()は指定したセクションに変数を配置するためのマクロです。 SEC(".maps")となっているので、ELFの.mapセクションに配置されることになります。
5.2 BPFプログラムのメイン処理
ここからは、opensnoop.bpf.cのメイン処理です。 opensnoopは、openシステムコールとopenatシステムコールの両方をトレースしているのですが、 やっていることは同じなので本記事ではopenに関する部分のみ解説します。
33 static __always_inline 34 bool trace_allowed(u32 tgid, u32 pid) 35 { 36 u32 uid; 37 38 /* filters */ 39 if (targ_tgid && targ_tgid != tgid) 40 return false; 41 if (targ_pid && targ_pid != pid) 42 return false; 43 if (valid_uid(targ_uid)) { 44 uid = (u32)bpf_get_current_uid_gid(); 45 if (targ_uid != uid) { 46 return false; 47 } 48 } 49 return true; 50 } 51 52 SEC("tracepoint/syscalls/sys_enter_open") 53 int tracepoint__syscalls__sys_enter_open(struct trace_event_raw_sys_enter* ctx) 54 { 55 u64 id = bpf_get_current_pid_tgid(); 56 /* use kernel terminology here for tgid/pid: */ 57 u32 tgid = id >> 32; 58 u32 pid = id; 59 60 /* store arg info for later lookup */ 61 if (trace_allowed(tgid, pid)) { 62 struct args_t args = {}; 63 args.fname = (const char *)ctx->args[0]; 64 args.flags = (int)ctx->args[1]; 65 bpf_map_update_elem(&start, &pid, &args, 0); 66 } 67 return 0; 68 } .... 88 static __always_inline 89 int trace_exit(struct trace_event_raw_sys_exit* ctx) 90 { 91 struct event event = {}; 92 struct args_t *ap; 93 int ret; 94 u32 pid = bpf_get_current_pid_tgid(); 95 96 ap = bpf_map_lookup_elem(&start, &pid); 97 if (!ap) 98 return 0; /* missed entry */ 99 ret = ctx->ret; 100 if (targ_failed && ret >= 0) 101 goto cleanup; /* want failed only */ 102 103 /* event data */ 104 event.pid = bpf_get_current_pid_tgid() >> 32; 105 event.uid = bpf_get_current_uid_gid(); 106 bpf_get_current_comm(&event.comm, sizeof(event.comm)); 107 bpf_probe_read_user_str(&event.fname, sizeof(event.fname), ap->fname); 108 event.flags = ap->flags; 109 event.ret = ret; 110 111 /* emit event */ 112 bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, 113 &event, sizeof(event)); 114 115 cleanup: 116 bpf_map_delete_elem(&start, &pid); 117 return 0; 118 } 119 120 SEC("tracepoint/syscalls/sys_exit_open") 121 int tracepoint__syscalls__sys_exit_open(struct trace_event_raw_sys_exit* ctx) 122 { 123 return trace_exit(ctx); 124 }
5.2.1 33-68行目
tracepoint__syscalls__sys_enter_open
は、openシステムコールの呼び出しをイベントとして呼ばれる関数です。
55行目のbpf_get_current_pid_tgid
はBPFヘルパー関数の1つで、呼び出した時点でのプロセスのスレッドIDとプロセスIDをカーネルが持つデータ構造から取得します。
処理の実体は、カーネル内に存在しています。
trace_allowed
を使って判定(61行目)し、トレース対象であればopenシステムコールの引数であるファイル名とフラグをBPFヘルパー関数の
bpf_map_update_elemを使ってmapに格納します。
5.2.2 88-124行目
tracepoint__syscalls__sys_exit_open
は、openシステムコールが終了するときに呼ばれる関数です。
trace_exit
では、tracepoint__syscalls__sys_enter_open
でmapに格納したデータを取り出し(96行目)、
さらにBPFアプリケーション側で必要になる情報をevent構造体にコピーしています(104-109行目)。
最終的に、BPFヘルパー関数であるbpf_perf_event_outputを呼び出し、BPFアプリケーションからデータが読みだせるようにします。
5.2.3 プログラムタイプとコンテキスト
BPFプログラムが呼ばれたときにカーネル側から渡される引数のことをコンテキストと呼びます。
tracepoint__syscalls__sys_exit_open
とtracepoint__syscalls__sys_enter_open
に渡されているctxがそれにあたります。
このコンテキストですが、何が渡されるかはプログラムタイプによって決まります。
プログラムタイプは、ユーザが作成したBPFプログラムの種別を表すもので、
カーネル内に定義値があり、BPFアプリケーションでBPFプログラムをロードする際に指定する必要があります。
プログラムタイプは、BPFプログラムから呼び出せるBPFヘルパー関数の種類や、そもそもどのタイミングでBPFプログラムがカーネルから呼ばれるかにも影響します。
プログラムタイプごとに呼び出し可能なBPFヘルパー関数は、BCCのドキュメントにまとめられています。
BPFアプリケーションがどのようにプログラムタイプを指定しているのかは後述しますが、先に結論を
書くとopensnoopは「BPF_PROG_TYPE_TRACEPOINT」を使用しています。
opensnoopのようにシステムコールに対して「BPF_PROG_TYPE_TRACEPOINT」を指定した場合、
BPFプログラムに渡されるコンテキストは以下のコマンドで調べることができます。
また、sys_exit_openに渡されるコンテキストは、sys_enter_openの部分をsys_exit_openに変更することで調べられます。
sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_open/format
5.3 opensnoop.bpf.cのコンパイル
「make opensnoop」と実行すると、以下の流れでopensnoopの実行形式ファイルが生成されます。
clang -g -O2 -Wall -target bpf -D__TARGET_ARCH_x86 -Ix86/ -I.output -I../src/cc/libbpf/include/uapi -c opensnoop.bpf.c -o .output/opensnoop.bpf.o && llvm-strip -g .output/opensnoop.bpf.o bin/bpftool gen skeleton .output/opensnoop.bpf.o > .output/opensnoop.skel.h cc -g -O2 -Wall -I.output -I../src/cc/libbpf/include/uapi -c opensnoop.c -o .output/opensnoop.o cc -g -O2 -Wall .output/opensnoop.o bcc/libbpf-tools/.output/libbpf.a .output/trace_helpers.o .output/syscall_helpers.o .output/errno_helpers.o .output/map_helpers.o .output/uprobe_helpers.o -lelf -lz -o opensnoop
1行目は、Clang/LLVMでopensnoop.bpf.cをBPFバイトコードとしてコンパイルしています。
2行目は、bpftoolというBPFの開発ツールを使って、opensnoop.bpf.oからopensnoop.skel.hというヘッダファイルを
生成しています。この*.skel.hというのは、スケルトンヘッダと言われopensnoop.bpf.oの骨組みを抽出したものになります。
3行目でopensnoop.cをコンパイルし、4行目でlibbpfなどとリンクしてopensnoopが完成します。
BPFプログラムはカーネルにロードする必要があるため、opensnoop自体にリンクはされていません。
BPFアプリケーション(opensnoop.c)がどうやってBPFプログラムを読み込むかは、2行目で生成されるスケルトンヘッダ
が関係しています。
6. opensnoop.cの解説
opensnoop.cは、opensnoopにおけるBPFアプリケーションにあたる部分です。
6.1、6.2章でBPFアプリケーションのメイン処理であるopensnoop.cの自体の解説を行い、
以降はメイン処理から呼ばれる各種処理について深追いしていきます。
自動生成のスケルトン部分とlibbpf内部の関数が入り乱れてしまいますが、
見分け方としては関数名が「opensnoop_」で始まるものがスケルトンヘッダ内に存在しているもので、
「bpf_」で始まるものがlibbpf内に存在しているものになります。
6.1 ヘッダファイルのインクルード
17 #include "opensnoop.h" 18 #include "opensnoop.skel.h" 19 #include "trace_helpers.h"
17行目でBPFプログラムと共有する構造体が定義されているopensnoop.hをインクルードしています。
また、18行目でBPFプログラムを使って自動生成したスケルトンヘッダをインクルードすることで、
スケルトンヘッダ内のバイトコードをメモリに読み込みます。
6.2 BPFアプリケーションのメイン処理
BPFアプリケーションのメイン処理は以下のようになっています。 処理概要としては以下のようになっています。
opensnoop_bpf__open
:スケルトンヘッダの読み込み、各種構造体の実体化、プログラムタイプの設定opensnoop_bpf__load
:mapの作成、BPFプログラムのカーネルへのロードopensnoop_bpf__attach
:BPFプログラムをイベントにアタッチperf_buffer__poll
:BPFプログラムがmapに書き込みをしたことを契機にハンドラを実行
215 int main(int argc, char **argv) 216 { ... 231 libbpf_set_strict_mode(LIBBPF_STRICT_ALL); 232 libbpf_set_print(libbpf_print_fn); 233 234 obj = opensnoop_bpf__open(); ... 241 obj->rodata->targ_tgid = env.pid; 242 obj->rodata->targ_pid = env.tid; 243 obj->rodata->targ_uid = env.uid; 244 obj->rodata->targ_failed = env.failed; ... 256 err = opensnoop_bpf__load(obj); ... 262 err = opensnoop_bpf__attach(obj); ... 278 /* setup event callbacks */ 279 pb = perf_buffer__new(bpf_map__fd(obj->maps.events), PERF_BUFFER_PAGES, 280 handle_event, handle_lost_events, NULL, NULL); ... 297 /* main: poll */ 298 while (!exiting) { 299 err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS); ... 308 } ... 315 }
6.3 231-232行目
231行目のlibbpf_set_strict_modeは、近い将来行われるlibbpfの大規模変更にあらかじめ適合するためのものです。 libbpfは2022年8月現在の最新版がv0.8.1ですが、v1.0では大規模な変更が行われるとアナウンスされています。
232行目は、opensnoopの出力関数の差し替えを行っています。 libbpf_print_fnは、vfprintfを使い標準エラー出力に出力しています。
6.4 opensnoop_bpf__open
opensnoop_bpf__open
は、スケルトンヘッダの読み込み、各種構造体の実体化、プログラムタイプの設定など様々な処理を行い、opensnoop_bpf構造体を構築します。
opensnoop_bpf構造体の定義はopensnoop.skel.hにあります。
11 struct opensnoop_bpf { 12 struct bpf_object_skeleton *skeleton; 13 struct bpf_object *obj; 14 struct { 15 struct bpf_map *start; 16 struct bpf_map *events; 17 struct bpf_map *rodata; 18 } maps; 19 struct { 20 struct bpf_program *tracepoint__syscalls__sys_enter_open; 21 struct bpf_program *tracepoint__syscalls__sys_enter_openat; 22 struct bpf_program *tracepoint__syscalls__sys_exit_open; 23 struct bpf_program *tracepoint__syscalls__sys_exit_openat; 24 } progs; 25 struct { 26 struct bpf_link *tracepoint__syscalls__sys_enter_open; 27 struct bpf_link *tracepoint__syscalls__sys_enter_openat; 28 struct bpf_link *tracepoint__syscalls__sys_exit_open; 29 struct bpf_link *tracepoint__syscalls__sys_exit_openat; 30 } links; 31 struct opensnoop_bpf__rodata { 32 __u64 min_us; 33 pid_t targ_pid; 34 pid_t targ_tgid; 35 uid_t targ_uid; 36 bool targ_failed; 37 } *rodata; 38 };
前述の通り、スケルトンヘッダはbpftoolによりBPFプログラムから自動生成されるものであるため、構造体メンバの名前の多くがBPFプログラムで設定したものになっています。
それぞれのメンバの役割は必要に応じて後述しますが、opensnoop_bpf__open
は、opensnoop_bpf構造体を作成して、値をセットするのがひとつの役割になっています。
opensnoop_bpf__open
は、以下のようにopensnoop_bpf__open_opts
を呼び出すだけです。
53 static inline struct opensnoop_bpf * 54 opensnoop_bpf__open_opts(const struct bpf_object_open_opts *opts) 55 { ... 59 obj = (struct opensnoop_bpf *)calloc(1, sizeof(*obj)); ... 65 err = opensnoop_bpf__create_skeleton(obj); ... 69 err = bpf_object__open_skeleton(obj->skeleton, opts); ... 78 } 79 80 static inline struct opensnoop_bpf * 81 opensnoop_bpf__open(void) 82 { 83 return opensnoop_bpf__open_opts(NULL); 84 }
59行目でopensnoop_bpf構造体をアロケートし、65行目でopensnoop_bpf__create_skeleton
を呼び出し、スケルトン構造体を構築します。
opensnoop_bpf__create_skeleton
は以下のようになっています。
124 static inline int 125 opensnoop_bpf__create_skeleton(struct opensnoop_bpf *obj) 126 { 127 struct bpf_object_skeleton *s; 128 129 s = (struct bpf_object_skeleton *)calloc(1, sizeof(*s)); 130 if (!s) 131 goto err; 132 obj->skeleton = s; 133 134 s->sz = sizeof(*s); 135 s->name = "opensnoop_bpf"; 136 s->obj = &obj->obj; 137 138 /* maps */ 139 s->map_cnt = 3; 140 s->map_skel_sz = sizeof(*s->maps); 141 s->maps = (struct bpf_map_skeleton *)calloc(s->map_cnt, s->map_skel_sz); 142 if (!s->maps) 143 goto err; 144 145 s->maps[0].name = "start"; 146 s->maps[0].map = &obj->maps.start; 147 148 s->maps[1].name = "events"; 149 s->maps[1].map = &obj->maps.events; 150 151 s->maps[2].name = "opensnoo.rodata"; 152 s->maps[2].map = &obj->maps.rodata; 153 s->maps[2].mmaped = (void **)&obj->rodata; ... 158 s->progs = (struct bpf_prog_skeleton *)calloc(s->prog_cnt, s->prog_skel_sz); 159 if (!s->progs) 160 goto err; 161 162 s->progs[0].name = "tracepoint__syscalls__sys_enter_open"; 163 s->progs[0].prog = &obj->progs.tracepoint__syscalls__sys_enter_open; 164 s->progs[0].link = &obj->links.tracepoint__syscalls__sys_enter_open; ... 170 s->progs[2].name = "tracepoint__syscalls__sys_exit_open"; 171 s->progs[2].prog = &obj->progs.tracepoint__syscalls__sys_exit_open; 172 s->progs[2].link = &obj->links.tracepoint__syscalls__sys_exit_open; ... 178 s->data = (void *)opensnoop_bpf__elf_bytes(&s->data_sz); ... 186 static inline const void *opensnoop_bpf__elf_bytes(size_t *sz) 187 { 188 *sz = 12752; 189 return (const void *) " 190 \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\ 191 \0\0\0\0\0\0\0\0\0\0\0\xd0\x2c\0\0\0\0\0\0\0\0\0\0\x40\0\0\0\0\0\x40\0\x14\0\ 192 \x01\0\xbf\x16\0\0\0\0\0\0\x85\0\0\0\x0e\0\0\0\x63\x0a\xfc\xff\0\0\0\0\x18\x01\
opensnoop_bpf__elf_bytes
でreturnしているバイト列がBPFプログラムのバイトコードになります。
後の工程でこのELFバイナリをlibelfで解析して、必要な構造体に移し替えたりして最終的に
カーネルにロードするような構造になっています。
opensnoopのリンクで-lelfを指定しているのはそのためです。
さて、opensnoop_bpf__create_skeleton
を呼び出した結果、opensnoop_bpf構造体は図1のようになります。
図1の通り、bpf_object_skeleton構造体の中身はほとんどがopensnoop_bpf構造体への参照になっていますが、 これは以降の工程でlibbpfの処理に入っていくため、opensnoop_bpf構造体というopensnoopに特化した構造体からbpf_object_skeleton構造体という汎用的な構造体へデータを関連付けるためです。
bpf_object__open_skeleton
は、以下のようになっています。
6957 static struct bpf_object *bpf_object_open(const char *path, const void *obj_buf, size_t obj_buf_sz, 6958 const struct bpf_object_open_opts *opts) 6959 { ... 6997 obj = bpf_object__new(path, obj_buf, obj_buf_sz, obj_name); ... 7032 err = err ? : bpf_object__init_maps(obj, opts); 7033 err = err ? : bpf_object_init_progs(obj, opts); ... 7044 } ... 7087 struct bpf_object * 7088 bpf_object__open_mem(const void *obj_buf, size_t obj_buf_sz, 7089 const struct bpf_object_open_opts *opts) 7090 { ... 7094 return libbpf_ptr(bpf_object_open(NULL, obj_buf, obj_buf_sz, opts)); 7095 } ... 11643 int bpf_object__open_skeleton(struct bpf_object_skeleton *s, 11644 const struct bpf_object_open_opts *opts) 11645 { ... 11664 obj = bpf_object__open_mem(s->data, s->data_sz, &skel_opts); ... 11672 *s->obj = obj; ... 11699 } ...
libbpfの中はコード量が多いため、解説を行わない箇所は大胆に割愛しています。
bpf_object__open_skeleton
は、
bpf_object__open_mem→bpf_object_openを呼びbpf_object構造体を作成します。
bpf_object構造体は、最終的にはbpfシステムコールを呼ぶときにカーネルに渡す引数を含んでいる重要な構造体です。
11672行目で、opensnoop_bpf構造体から参照できるようにセットされています。
6997行目のbpf_object__newではbpf_object構造体をアロケートとopensnoop.bpf.oのELFバイナリのバイト列の参照のセットなどを行っています。
6.4.1 bpf_object__init_maps
7032行目のbpf_object__init_mapsでは、mapの初期化の過程でBPFプログラム内のSEC(".maps")を使って宣言した変数に対する処理を以下の流れで行っています。
bpf_object__init_maps→bpf_object__init_user_btf_maps→bpf_object__init_user_btf_map→parse_btf_map_def
最終的には、BPFプログラム内で宣言したstartやeventsは、bpf_object構造体のmaps配列に格納され、後工程でbpfシステムコールを呼んでmapを作成する際に使われます。
また、以下の流れでBPFプログラム内でconst付きで宣言していたグローバル変数に関する処理も行っています。
bpf_object__init_maps→bpf_object__init_global_data_maps→bpf_object__init_internal_map
BPFプログラム内でグローバル変数に代入していた初期値は、1579行目でmmapシステムコールで確保した領域にコピーされています。
6.4.2 bpf_object_init_progs
6931行目でカーネルにロードするプログラムのプログラムタイプをセットしています。
前述の通り、opensnoopは「BPF_PROG_TYPE_TRACEPOINT」をセットするわけですが、
これは実はBPFプログラム内にあるSEC("tracepoint/syscalls/sys_enter_open")という指定方法が関係しています。
6923行目のfind_sec_defでは、以下のsection_defsテーブルを探索しセクション名がsection_defsテーブルに指定した文字列リテラルと一致するオブジェクトを探索しています。 その結果、以下のテーブルの8585行目に一致し、prog->sec_def->prog_typeが「BPF_PROG_TYPE_TRACEPOINT」になるというわけです。
8558 #define SEC_DEF(sec_pfx, ptype, atype, flags, ...) { \ 8559 .sec = sec_pfx, \ 8560 .prog_type = BPF_PROG_TYPE_##ptype, \ 8561 .expected_attach_type = atype, \ 8562 .cookie = (long)(flags), \ 8563 .preload_fn = libbpf_preload_prog, \ 8564 __VA_ARGS__ \ 8565 } ... 8574 static const struct bpf_sec_def section_defs[] = { 8575 SEC_DEF("socket", SOCKET_FILTER, 0, SEC_NONE | SEC_SLOPPY_PFX), ... 8578 SEC_DEF("kprobe/", KPROBE, 0, SEC_NONE, attach_kprobe), 8579 SEC_DEF("uprobe/", KPROBE, 0, SEC_NONE), 8580 SEC_DEF("kretprobe/", KPROBE, 0, SEC_NONE, attach_kprobe), 8581 SEC_DEF("uretprobe/", KPROBE, 0, SEC_NONE), ... 8585 SEC_DEF("tracepoint/", TRACEPOINT, 0, SEC_NONE, attach_tp), 8586 SEC_DEF("tp/", TRACEPOINT, 0, SEC_NONE, attach_tp), ...
6.5 241-245行目
ユーザが引数で指定したフィルタのオプション値を代入しています。
BPFプログラム内でtarg_tgidやtarg_pidなどを参照していますが、最終的にはここで設定した値を参照しています。
BPFプログラムのコンパイル単位では、const付きで宣言していたので変更不可ですが
obj->rodataで参照するのは、bpf_object__init_internal_map内でmmapが確保した領域になっているため、変更ができるというわけです。
ただグローバル変数の変更が有効なのはopensnoop_bpf__open
後、opensnoop_bpf__load
前の区間だけになります。
opensnoop_bpf__load
以降は、BPFプログラムはカーネルにロードされるため、BPFプログラム内で参照している変数の変更ができないためです。
6.6 opensnoop_bpf__load
opensnoop_bpf__load
は、スケルトンヘッダファイルの中に処理がありますが、
やっていることは以下のようにlibbpfのbpf_object__load_skeletonを呼ぶだけです。
86 static inline int 87 opensnoop_bpf__load(struct opensnoop_bpf *obj) 88 { 89 return bpf_object__load_skeleton(obj->skeleton); 90 }
bpf_object__load_skeletonは bpf_object__load→bpf_object_loadを呼びます。
7464 static int bpf_object_load(struct bpf_object *obj, int extra_log_level, const char *target_btf_path) 7465 { ... 7485 err = err ? : bpf_object__create_maps(obj); ... 7487 err = err ? : bpf_object__load_progs(obj, extra_log_level); ... 7530 }
6.6.1 bpf_object__create_maps
bpf_object_loadでは、7485行目でbpfシステムコールを使ってmapを作成しています。
bpf_object__create_maps→bpf_object__create_map→bpf_map_create→sys_bpf_fd→sys_bpf
という流れで処理され、最終的にはbpfシステムコールをcmd=BPF_MAP_CREATEで呼び出すことで、mapを作成しています。
ちなみにrodataセクションに配置されたmapについては、
ここでbpf_object__populate_internal_mapを呼び出し、BPF_MAP_FREEZEでbpfシステムコールを呼び出すことで、read onlyにしています。
6.6.2 bpf_object__load_progs
bpf_object__load_progsでは、tracepoint__syscalls__sys_enter_open
などのBPFプログラムのロジック部分をカーネルにロードします。
bpf_object__load_progs→bpf_object_load_prog→bpf_object_load_prog_instance→bpf_prog_load→sys_bpf_prog_load
の流れで処理され、最終的にはbpfシステムコールをcmd=BPF_PROG_LOADで呼び出すことで、プログラム部分をカーネルにロードしています。
6.7 opensnoop_bpf__attach
opensnoop_bpf__load
でbpfシステムコールを呼び出し、Verifierで検証された後でJITコンパイルが行われネイティブコードになりカーネルのロードが完了します。
イベント発生時にBPFプログラムを動作させるには、ロードしたBPFプログラムをフックポイントにアタッチする必要があります。
これを行うのがopensnoop_bpf__attach
です。
opensnoop_bpf__attach
のコードは、スケルトンヘッダに存在し、以下のようにlibbpfのbpf_object__attach_skeletonを呼ぶだけです。
110 static inline int 111 opensnoop_bpf__attach(struct opensnoop_bpf *obj) 112 { 113 return bpf_object__attach_skeleton(obj->skeleton); 114 }
bpf_object__attach_skeletonはカーネルにロードした個々のBPFプログラムについて、bpf_program__attachを呼びます。
attach_fnは、ここで宣言されており、BPFプログラムをどこのセクションに配置したかで変わります。
tracepointセクションに配置された場合に呼ばれるattach_tpは、内部で
bpf_program__attach_tracepoint→bpf_program__attach_tracepoint_optsと処理が進められます。
bpf_program__attach_tracepoint_optsがアタッチ処理のメイン部分になっていて、以下のようになっています。
10318 struct bpf_link *bpf_program__attach_tracepoint_opts(const struct bpf_program *prog, 10319 const char *tp_category, 10320 const char *tp_name, 10321 const struct bpf_tracepoint_opts *opts) 10322 { ... 10333 pfd = perf_event_open_tracepoint(tp_category, tp_name); ... 10340 link = bpf_program__attach_perf_event_opts(prog, pfd, &pe_opts); ... 10350 }
perf_event_open_tracepointでperf_event_openシステムコールを呼び出し、PERF_TYPE_TRACEPOINTに対応したファイルディスクリプタを作成します。
perfとは、Linuxにおけるパフォーマンスモニタリングを行うための仕組みです。
CPUのカウンタ値を取得する際などに使用するperfコマンドは、内部でperf_event_openシステムコールを呼び出しているようです。
上記の通り、BPFでtracepointにアタッチする場合だと、perfの仕組みを利用していますが、
例えばBPFプログラム内のSEC()で"kprobe/"のように指定した場合は、kprobeの仕組みを利用します。
libbpf-toolsだと、例えばbindsnoopなどがkprobeの仕組みを使用しています。
bpf_program__attach_perf_event_opts→bpf_link_createと処理が行われ、最終的にはbpfシステムコールをcmd=BPF_LINK_CREATEで呼び出してアタッチしています。 ただし、カーネル側がBPF_LINK_CREATEに対応していない場合は、ioctl(PERF_EVENT_IOC_SET_BPF)でアタッチするようです。 その後、ioctl(PERF_EVENT_IOC_ENABLE)でperfイベントを有効することで、perfイベントが発生したときにBPFプログラムが実行されるようにしています。
6.8 279-308行目
アタッチが完了すれば、あとはBPFプログラムがmapに出力する内容をBPFアプリケーション側で読み取って出力するだけです。
perf_buffer__new
とperf_buffer__pollは、まさにそれを行っており、
perf_buffer__new
でポーリングするmapとイベントが発生したときに呼び出されるハンドラ(handle_event)をセットし、
終了条件を満たすまでperf_buffer__pollでイベント(mapへの書き込み)を待っています。
画面出力部分は、総じて難しいことは行っていないので割愛します。
また余談ですが、現在ではopensnoopで使用しているperf_bufferを使ったやり方よりも、ringbufを使う方がいいみたいです。本記事はopensnoopの解説がテーマなのでringbufは追及しません。
さて、perf_buffer__new
は関数マクロになっており、実際にはperf_buffer__new_v0_6_0が呼ばれ、そこからメイン処理である
__perf_buffer__newが呼ばれます。
__perf_buffer__newでは、cpu*という変数名がありますが、これはperf_bufferがCPUごとに持つバッファのためです。
__perf_buffer__newは以下のようになっています。
10945 static struct perf_buffer *__perf_buffer__new(int map_fd, size_t page_cnt, 10946 struct perf_buffer_params *p) 10947 { ... 10965 err = bpf_obj_get_info_by_fd(map_fd, &map, &map_info_len); ... 10991 pb->sample_cb = p->sample_cb; ... 10999 pb->epoll_fd = epoll_create1(EPOLL_CLOEXEC); ... 11038 for (i = 0, j = 0; i < pb->cpu_cnt; i++) { ... 11050 11051 cpu_buf = perf_buffer__open_cpu_buf(pb, p->attr, cpu, map_key); ... 11059 err = bpf_map_update_elem(pb->map_fd, &map_key, 11060 &cpu_buf->fd, 0); ... 11068 11069 pb->events[j].events = EPOLLIN; 11070 pb->events[j].data.ptr = cpu_buf; 11071 if (epoll_ctl(pb->epoll_fd, EPOLL_CTL_ADD, cpu_buf->fd, 11072 &pb->events[j]) < 0) { ... 11078 } ... 11080 } ... 11091 }
10965行目でカーネル内に作成したmapのファイルディスクリプタを取得しています。
このmapはBPFプログラムでeventsとして宣言していたものです。
その後、pb(perf_buffer構造体)をアロケートし、10991行目でp->sample_cbを代入していますが、
このsample_cbがopensnoop.cから渡されるhandle_eventになります。
10999行目でepollのファイルディスクリプタを作成していますが、このファイルディスクリプタが後でperf_buffer__pollするときにイベント待ちに使われるものです。
11051行目でperf_buffer__open_cpu_bufを呼び、CPU数分バッファの作成とperf_event_openシステムコールの呼び出しなどを行っています。
perf_buffer__open_cpu_bufでやっていることは、BPFプログラム側で呼んでいるbpf_perf_event_outputを使うために必要なことのようです。
bpf_perf_event_outputのマニュアルにユーザ空間で読みだすためにはどのような手続きが必要かの記載があります。
11059行目でeventsとCPUごとのバッファを紐づけています。keyは、ループのインデックス(i)です。
11071行目でepoll_ctlを呼び、CPUごとのバッファをepollの監視対象にします。
次にperf_buffer__pollですが、以下のようなコードになっています。
11106 static enum bpf_perf_event_ret 11107 perf_buffer__process_record(struct perf_event_header *e, void *ctx) 11108 { 11109 struct perf_cpu_buf *cpu_buf = ctx; 11110 struct perf_buffer *pb = cpu_buf->pb; 11111 void *data = e; ... 11117 switch (e->type) { 11118 case PERF_RECORD_SAMPLE: { 11119 struct perf_sample_raw *s = data; 11120 11121 if (pb->sample_cb) 11122 pb->sample_cb(pb->ctx, cpu_buf->cpu, s->data, s->size); 11123 break; 11124 } ... 11135 } 11136 return LIBBPF_PERF_EVENT_CONT; 11137 } ... 11139 static int perf_buffer__process_records(struct perf_buffer *pb, 11140 struct perf_cpu_buf *cpu_buf) 11141 { ... 11144 ret = perf_event_read_simple(cpu_buf->base, pb->mmap_size, 11145 pb->page_size, &cpu_buf->buf, 11146 &cpu_buf->buf_size, 11147 perf_buffer__process_record, cpu_buf); ... 11151 } ... 11158 int perf_buffer__poll(struct perf_buffer *pb, int timeout_ms) 11159 { ... 11162 cnt = epoll_wait(pb->epoll_fd, pb->events, pb->cpu_cnt, timeout_ms); ... 11165 11166 for (i = 0; i < cnt; i++) { 11167 struct perf_cpu_buf *cpu_buf = pb->events[i].data.ptr; 11168 11169 err = perf_buffer__process_records(pb, cpu_buf); ... 11174 } ... 11176 }
11162行目でepoll_waitでCPUごとのバッファが読み出し可能になるかタイムアウト(100ミリ秒)になるまでブロックします。
バッファに書き込みが発生したら、11167行目でバッファへのポインタを取り出してperf_buffer__process_recordsを呼び出します。
__perf_buffer__newで作成したバッファは、リングバッファとして使用されており、perf_event_read_simpleでは、実際にバッファに書き込まれたデータを取り出し、
perf_buffer__process_recordに渡します。
最終的には、こちらで呼ばれるsample_cbの呼び出しが、opensnoop.cで渡したhandle_eventになっており、BPFプログラムから渡されたデータの出力が行われます。
7. おわりに
BPFは、ツールも含めまだまだ絶賛開発中の技術です。
昨年は、WindowsでBPFをサポートすることをマイクロソフトが発表するなどLinux以外のOSでもBPFの仕組みは広がりつつあります。
ちなみにBPFがどのような方向に進んでいっているのかは、
Linux Kernel Developers' bpfconf 2022の資料が参考になるかもしれません。
8. 参考文献
Liz Rice. What is eBPF? . O'Reilly Media, Inc, April 2022,
*1:2022年8月11日にv0.25.0がリリースされましたが、本記事では執筆時点で最新だったv0.24.0を対象にしています