macOSでもBCCで遊んでみたい
eBPFプログラムを開発する一般的な方法はBCC (BPF compiler collection)を使うことである。Linux環境だとDockerで簡単に環境も構築できるが、今回はmacOS上に環境構築してみる。
macOSローカルでもBCCを動かして遊んでみるために、Vagrantスクリプトを公開している人がいたので、Ubuntuのバージョンを18.04から20.04にしてみた(vagrant-bcctools)。バックエンドはVirtualboxを使っている。BCC自体はC++で実装されていて、C/C++から使うことはできるが、PythonやLuaバインドが提供されている。
vagrant@vagrant:/usr/share/doc/bpfcc-tools/examples$ cat tracing/trace_fields.py
#!/usr/bin/python
# Copyright (c) PLUMgrid, Inc.
# Licensed under the Apache License, Version 2.0 (the "License")
# This is an example of tracing an event and printing custom fields.
# run in project examples directory with:
# sudo ./trace_fields.py"
from __future__ import print_function
from bcc import BPF
prog = """
int hello(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
print("PID MESSAGE")
try:
b.trace_print(fmt="{1} {5}")
except KeyboardInterrupt:
exit()
Pythonバインディングのサンプルコードである。progがBPFプログラムで、この部分はCで記述する必要があるけど、これをコンパイルしてロードしたり、フックポイントにアタッチする部分はPythonで記述できる。やっていることは何となく想像できると思うが、cloneシステムコールが呼ばれたら、hello関数を実行する。
Disassembly of section .bpf.fn.hello:
hello:
; int hello(void *ctx) { // Line 13
0: b7 01 00 00 21 0a 00 00 r1 = 2593
; ({ char _fmt[] = "Hello, World!\n"; bpf_trace_printk_(_fmt, sizeof(_fmt)); }); // Line 15
1: 6b 1a fc ff 00 00 00 00 *(u16 *)(r10 - 4) = r1
2: b7 01 00 00 6f 72 6c 64 r1 = 1684828783
3: 63 1a f8 ff 00 00 00 00 *(u32 *)(r10 - 8) = r1
4: 18 01 00 00 48 65 6c 6c 00 00 00 00 6f 2c 20 57 r1 = 6278066737626506568 ll
6: 7b 1a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r1
7: b7 01 00 00 00 00 00 00 r1 = 0
8: 73 1a fe ff 00 00 00 00 *(u8 *)(r10 - 2) = r1
9: bf a1 00 00 00 00 00 00 r1 = r10
10: 07 01 00 00 f0 ff ff ff r1 += -16
11: b7 02 00 00 0f 00 00 00 r2 = 15
12: 85 00 00 00 06 00 00 00 call 6
; return 0; // Line 16
13: b7 00 00 00 00 00 00 00 r0 = 0
14: 95 00 00 00 00 00 00 00 exit
BPF(text=prog, debug=0x8)とかやって実行すると、BPFプログラムをディスアセンブルしたものが表示される。
eBPF仮想マシンはcBPFから大きく拡張されていて、11本の64ビットレジスタ(r0〜r11)とマップ(ストレージ)を持つ。レジスタのうちr10はローカル変数を参照するためのスタックフレームレジスタとして使う。マップというのはいろんな種類が定義されているのだけど、ユーザ空間とカーネル空間や、eBPFプログラム間でデータを授受するのに使われる。さらに各種ヘルパー関数を呼び出すことができるようになった。
include/uapi/linux/bpf.h
65 struct bpf_insn {
66 __u8 code; /* opcode */
67 __u8 dst_reg:4; /* dest register */
68 __u8 src_reg:4; /* source register */
69 __s16 off; /* signed offset */
70 __s32 imm; /* signed immediate constant */
71 };
eBPFでは命令セットが64ビットに拡張されている。eBPFの命令セットを深追いしたかったらこれがわかりやすい。
0〜8行目ではr10-16〜r10の領域に"Hello, world!\n"の文字列を格納し、12行目でヘルパーファンクションであるtrace_printをcallしている。eBPFの呼び出し規則として、引数はレジスタ渡しになる。r1が第一引数、r2が第二引数。。。戻り値はr0渡しかな。
$ sudo bpftool prog dump xlated id 10
int kprobe__sys_clone(void * ctx):
; int kprobe__sys_clone(void *ctx) {
0: (b7) r1 = 2593
; ({ char _fmt[] = "Hello, World!\n"; bpf_trace_printk_(_fmt, sizeof(_fmt)); }); return 0; }
1: (6b) *(u16 *)(r10 -4) = r1
2: (b7) r1 = 1684828783
3: (63) *(u32 *)(r10 -8) = r1
4: (18) r1 = 0x57202c6f6c6c6548
6: (7b) *(u64 *)(r10 -16) = r1
7: (b7) r1 = 0
8: (73) *(u8 *)(r10 -2) = r1
9: (bf) r1 = r10
;
10: (07) r1 += -16
; ({ char _fmt[] = "Hello, World!\n"; bpf_trace_printk_(_fmt, sizeof(_fmt)); }); return 0; }
11: (b7) r2 = 15
12: (85) call bpf_trace_printk#-55792
; ({ char _fmt[] = "Hello, World!\n"; bpf_trace_printk_(_fmt, sizeof(_fmt)); }); return 0; }
13: (b7) r0 = 0
14: (95) exit
bpftoolを使うとverify後のコードを調べることができる。今回のケースは変わりがないな。
この記事が気に入ったらサポートをしてみませんか?