bpf や bcc 周りは使わないと忘れてしまうのでメモ。
Hello, World
clang で bpf オブジェクトを作って C でそれを読み込む…という方法よりも https://github.com/iovisor/bcc/ を使う方が楽なので、bcc でやります。
C でやると環境作るところから大変なので… ref : https://blog.raymond.burkholder.net/index.php?/archives/1000-eBPF-Basics.html
execve システムコールが呼ばれたときに Hello, World!
と表示するプログラムを作ります。
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
int do_trace_execve(struct pt_regs *ctx,
const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
b = BPF(text=bpf_text)
execve_fnname = b.get_syscall_fnname("execve")
b.attach_kprobe(event=execve_fnname, fn_name="do_trace_execve")
while 1:
try:
pass
except KetboardInterrupt:
exit()
上記プログラムを実行します。
$ sudo python helloworld.py
bpf_trace_printk()
は /sys/kernel/debug/tracing/trace_pipe
に出力されます。
別のターミナルで ls
や id
など適当なコマンドを打つと、/sys/kernel/debug/tracing/trace_pipe
に Hello, World!
が出力されます。
# cat /sys/kernel/debug/tracing/trace_pipe
bash-4257 [000] d... 53488.525874: : Hello, World!
bash-4258 [000] d... 53490.627675: : Hello, World!
bcc には trace_print()
という関数があり、これを使うことで /sys/kernel/debug/traceing/trace_pipe
の情報を出力してくれる。
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
int do_trace_execve(struct pt_regs *ctx,
const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
b = BPF(text=bpf_text)
execve_fnname = b.get_syscall_fnname("execve")
b.attach_kprobe(event=execve_fnname, fn_name="do_trace_execve")
while 1:
try:
b.trace_print()
except KetboardInterrupt:
exit()
BPF プログラムの種類
BPF プログラムにはいくつか種類がある。
- Socket Filter Program
BPF_PROG_TYPE_SOCKET_FILTER
- Kprobe Program
BPF_PROG_TYPE_KPROBE
- Tracepoint Programs
BPF_PROG_TYPE_TRACEPOINT
- XDP Programs
BPF_PROG_TYPE_XDP
- Perf Event Programs
BPF_PROG_TYPE_PERF_EVENT
- Perf がデータを生成するたびに bpf コードが呼ばれる
- Cgroup Socket Programs
BPF_PROG_TYPE_CGROUP_SKB
- bpf ロジックを cgroup につなげることができる。具体的には cgroups 内のプロセスにネットワークが流れる前にパケットの処理を決定できる。Cillium が使っているらしい。
- Cgroup Open Socket Programs
BPF_PROG_TYPE_CGROUP_SOCK
- cgroup 内のプロセスがソケットを開いたときにコードを実行できる。
上記以外にもたくさんあるので、詳しくは
https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/bpf.h#L149 を参照。
BPF Maps
Kernel にある key/value store のようなもの。user-space で動くプログラムはこれにファイルディスクリプタでアクセス可能。事前に型を決めておけば、どのようなデータも保存できる。
union bpf_attr my_map {
.map_type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(int),
.max_entries = 100,
.map_flags = BPF_F_NO_PREALLOC,
};
int fd = bpf(BPF_MAP_CREATE, &my_map, sizeof(my_map));
もし呼び出しが失敗するとカーネルは exit code -1 を返す。
これは3つの理由により失敗する可能性がある。
- 属性が無効である場合、errno に EINVAL をセットする
- 権限がない場合は EPERM
- マップを保存するのに十分なメモリがない場合は ENOMEM
上記コードをより簡素に書くには次のようにする。
int fd;
fd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100,
BPF_F_NO_PREALOC);
struct bpf_map_def SEC("maps") my_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(int),
.max_entries = 100,
.map_flags = BPF_F_NO_PREALLOC,
};
map に情報を入力するには bpf_map_update_elem
を使う。対して、削除を行う場合は bpf_map_delete_element
を使う。検索する場合は bpf_map_lookup_elem
などもあり、key/value store として十分に使える命令がある。
上記は C の話で bcc を使う場合は、次のようになる。
execve
に入ったときに map に値を保存し、出たときに map から値を出力するようなプログラムを例に示す。
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
BPF_HASH(hash);
int do_trace_execve(struct pt_regs *ctx,
const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
u64 key, value;
key = 1, value = 1234;
hash.update(&key, &value);
return 0;
}
int do_trace_ret_execve(struct pt_regs *ctx)
{
u64 *p;
u64 key = 1;
p = hash.lookup(&key);
if (p == NULL) {
bpf_trace_printk("Not found\\n");
return 0;
}
bpf_trace_printk("%d\\n", *p);
return 0;
}
"""
b = BPF(text=bpf_text)
execve_fnname = b.get_syscall_fnname("execve")
b.attach_kprobe(event=execve_fnname, fn_name="do_trace_execve")
b.attach_kretprobe(event=execve_fnname, fn_name="do_trace_ret_execve")
while 1:
try:
b.trace_print()
except KeyboardInterrupt:
exit()
これの実行結果は次のようになる。
$ sudo python map.py
<...>-4300 [001] d... 55143.237657: : 1234
id-4301 [000] d... 55144.061994: : 1234
id-4302 [000] d... 55144.640731: : 1234
イテレーション
map のキーが分からない場合や全列挙したい場合に map 自体をイテレーションしたい場合がある。 bpf_map_get_next_key
という命令があるが、bcc だと tables.items()
があり、大変便利。
counts = b.get_table("hash")
for k, v in counts.items():
print("key: %d, value: %d" % (k.value, v.value))
time.sleep(3)
key: 2, value: 5678
key: 3, value: 9012
key: 1, value: 1234
BPF Spin Lock
map に対して複数のプログラムがアクセスすると競合状態になってしまう。
そのために BPF には BPF Spin Lock という概念があり、map へのアクセスをロックすることができる。
これを利用するには map に bpf_spin_lock
構造体を追加する必要がある。
struct data_t {
struct bpf_spin_lock semaphore;
int count;
}
そして bpf_spin_lock()
, bpf_spin_unlock()
でロック/アンロックができる。
struct data_t data = {};
key = 1, data.value = 1234;
hash.update(&key, &data);
...
p = hash.lookup(&key);
bpf_spin_lock(&p->semaphore);
p->value += 100;
bpf_spin_unlock(&p->semaphore);
ただし、 bpf_spin_lock()
が入ったのは Kernel 5.1 からなので、それ以前では利用できない。
自分の環境は 4.9 だったので上記コードは試していない……
BPF Map の種類
map にも種類があって、https://elixir.bootlin.com/linux/v5.3.9/source/include/uapi/linux/bpf.h#L111 に定義がある。
BPF Map の永続化
プログラムが終了すると map が消えてしまい、困ることがある。
そこでファイルシステムに保存することが可能となっている。パスとして /sys/fs/bpf
が使われることが多い。
保存するには bpf_obj_pin()
を、逆に取得するには bpf_obj_get()
を使う。
bcc ではこのインターフェイスがないので、libbcc を使う。
from bcc import BPF, libbcc
import ctypes
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
BPF_HASH(counts);
int do_trace_execve(struct pt_regs *ctx,
const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
u64 key = 1;
u64 zero = 0;
u64 *val;
val = counts.lookup_or_try_init(&key, &zero);
if (val) {
bpf_trace_printk("%d\\n", *val);
}
counts.increment(key);
return 0;
}
"""
b = BPF(text=bpf_text)
pin_path = "/sys/fs/bpf/counter"
execve_fnname = b.get_syscall_fnname("execve")
b.attach_kprobe(event=execve_fnname, fn_name="do_trace_execve")
h = b.get_table("counts")
ret = libbcc.lib.bpf_obj_pin(h.map_fd, ctypes.c_char_p(pin_path))
if ret != 0:
raise Exception("Failed to pin map")
while 1:
try:
b.trace_print()
except KeyboardInterrupt:
exit()
上記プログラムは /sys/fs/bpf/counter
に execve の数を記録するだけのもの。
実行すると、確かにファイルが作成される。
$ sudo python pin.py
bash-4660 [000] d... 66514.151213: : 0
bash-4661 [000] d... 66516.259077: : 1
bash-4662 [000] d... 66517.417327: : 2
$ ls -al /sys/fs/bpf/counter
-rw------- 1 root root 0 Dec 18 06:55 /sys/fs/bpf/counter
保存した map を取得するには bpf_obj_get()
を使う。
from bcc import libbcc, table
import ctypes
path = "/sys/fs/bpf/counter"
class PinnedArray(table.Array):
def __init__(self, path, keytype, leaftype, max_entries):
map_fd = libbcc.lib.bpf_obj_get(ctypes.c_char_p(path))
if map_fd < 0:
raise ValueError("Failed to open eBPF map")
self.map_fd = map_fd
self.Key = keytype
self.Leaf = leaftype
self.max_entries = max_entries
counts = PinnedArray(
path = path,
keytype = ctypes.c_uint64,
leaftype = ctypes.c_uint64,
max_entries = 10240,
)
print(counts.values()[0].value) # 3
Probe
コード中に b.attach_kprobe(event=execve_fnname, fn_name="do_trace_execve")
というのがあるが、このトレースする場所のことを Probe と呼ぶ。
Probe には次の種類がある。
- Kernel probes
- Tracepoints
- User-space probes
- USDT ( User statically defined tracepoints )
Kernel Probe
Kernel Probe には2種類あり、kprobe とkretprobe がある。
kprobe は命令が実行される前に BPF プログラムを挿入できるのに対し、kretprobe は値を返すときに BPF プログラムが挿入される。
それぞれ bcc では attach_kprobe()
と attach_kretprobe()
で probe と probe に到達したときに実行する関数を指定できる。
probe に到達したときに実行される関数の第一引数は pt_regs
構造体で、ここにはレジスタと BPF のコンテキストが格納される。 PT_REGS_RC
などを用いてアクセスできる。
int ret = PT_REGS_RC(ctx);
if (ret != 0) {
return ret;
}
PT_REGS_*
なマクロ https://elixir.bootlin.com/linux/v4.9/source/samples/bpf/bpf_helpers.h#L94 で確認できる。
Tracepoints
Tracepoints は既に埋め込まれている Probe を利用する。既に埋め込まれているという点で、kprobe と比べて静的と言える。Kernel 側で用意されているので、カーネルのバージョンが異なっても動くことが保証される。
Tracepoints は /sys/kernel/debug/tracing/events
配下で確認できる。
$ sudo ls /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve
enable filter format id trigger
enable
は他のプロセスに対してもトレースを行うかどうか。0は無効で、1で有効になる。デフォルトは0。
詳しくは https://www.kernel.org/doc/html/v4.18/trace/events.html にまとまっている。
from __future__ import print_function
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
struct sys_enter_execve_arg {
u64 __unused__;
const char * filename;
const char * argv;
const char * envp;
};
int do_trace(struct sys_enter_execve_arg *args) {
return 0;
};
"""
b = BPF(text=bpf_text)
b.attach_tracepoint("syscalls:sys_enter_execve", "do_trace")
b.trace_print()
上記プログラムを実行すると、 format
に従ったフォーマットで出力される。
bash-5148 [000] .... 88796.569831: sys_execve(filename: b13288, argv: c87208, envp: be4008)
sudo-5149 [001] .... 88796.579812: sys_execve(filename: 55c77c58fe38, argv: 55c77c58fe58, envp: 55c77c582590)
<...>-5150 [000] .... 88799.058127: sys_execve(filename: 1fea388, argv: 1f96c68, envp: 1f32008)
Userspace Probes
ユーザー空間で実行されているプロセスに対して動的に Attach できる。こちらも Kernel Probe と同様に、uprobe と uretprobe の2つがある。
適当なバイナリを作り、シンボルを選ぶ。
$ cat main.go
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Hello")
sleep()
}
func sleep() {
time.Sleep(30 * time.Second)
}
$ env GOOS=linux GOARCH=amd64 go build main.go
$ objdump -t main | grep sleep
00000000004099a0 g F .text 00000000000000f0 runtime.notesleep
0000000000409a90 g F .text 00000000000001aa runtime.notetsleep_internal
0000000000409c40 g F .text 0000000000000088 runtime.notetsleep
0000000000409cd0 g F .text 0000000000000091 runtime.notetsleepg
00000000004271a0 g F .text 00000000000000ce runtime.futexsleep
0000000000455180 g F .text 0000000000000047 runtime.usleep
runtime.usleep
をトレースする。
from bcc import BPF
bpf_source = """
#include <uapi/linux/ptrace.h>
int trace_go_main(struct pt_regs *ctx)
{
u64 pid = bpf_get_current_pid_tgid();
bpf_trace_printk("New main process running with PID: %d\\n", pid);
return 0;
}
"""
bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name="./main", sym="runtime.usleep", fn_name="trace_go_main")
bpf.trace_print()
main-5616 [000] .... 110846.900579: : New main process running with PID: 5616
ref : http://www.brendangregg.com/blog/2017-01-31/golang-bcc-bpf-function-tracing.html
USDT
USDT については以前ブログを書いた。https://blog.ssrf.in/post/using-usdt/
こういうのもある。https://github.com/dalehamel/ruby-static-tracing
Perf Event
bcc では BPF_PERF_OUTPUT()
で Perf Ring Buffer にデータを送る。これは User-space で動いているプロセスにデータを渡すときによく使われる方法。
例えば execve をトレースし、その pid と comm を User-space プロセスに渡すには次のようになる。
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
BPF_PERF_OUTPUT(events);
struct data_t {
char comm[TASK_COMM_LEN];
u32 pid;
};
int do_trace(struct pt_regs *ctx,
const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
"""
bpf = BPF(text=bpf_text)
execve_fnname = bpf.get_syscall_fnname("execve")
bpf.attach_kretprobe(event=execve_fnname, fn_name="do_trace")
def print_event(cpu, data, size):
event = bpf["events"].event(data)
print("pid: %d, comm: %s" % (event.pid, event.comm))
bpf["events"].open_perf_buffer(print_event)
while 1:
try:
bpf.perf_buffer_poll()
except KeyboardInterrupt:
exit()
このコードを実行すると、ちゃんと pid と comm が表示される。
pid: 5669, comm: id
pid: 5670, comm: ls
pid: 5671, comm: uname
長くなったので、一旦ここまで。