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後のコードを調べることができる。今回のケースは変わりがないな。

この記事が気に入ったらサポートをしてみませんか?