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 に出力されます。

別のターミナルで lsid など適当なコマンドを打つと、/sys/kernel/debug/tracing/trace_pipeHello, 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 プログラムにはいくつか種類がある。

  1. Socket Filter Program BPF_PROG_TYPE_SOCKET_FILTER
  2. Kprobe Program BPF_PROG_TYPE_KPROBE
  3. Tracepoint Programs BPF_PROG_TYPE_TRACEPOINT
  4. XDP Programs BPF_PROG_TYPE_XDP
  5. Perf Event Programs BPF_PROG_TYPE_PERF_EVENT
    1. Perf がデータを生成するたびに bpf コードが呼ばれる
  6. Cgroup Socket Programs BPF_PROG_TYPE_CGROUP_SKB
    1. bpf ロジックを cgroup につなげることができる。具体的には cgroups 内のプロセスにネットワークが流れる前にパケットの処理を決定できる。Cillium が使っているらしい。
  7. Cgroup Open Socket Programs BPF_PROG_TYPE_CGROUP_SOCK
    1. 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つの理由により失敗する可能性がある。

  1. 属性が無効である場合、errno に EINVAL をセットする
  2. 権限がない場合は EPERM
  3. マップを保存するのに十分なメモリがない場合は 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 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

長くなったので、一旦ここまで。