ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

BPF CO-RE バイナリはどのようにビルドされるか

そろそろBPFのことを思い出そうと思って記事を書く。

シリーズです:

udzura.hatenablog.jp

udzura.hatenablog.jp

前回まででRustでのBPF CO-REバイナリの作成方法をまとめたが、C言語などで使う場合の細かい手順を追ってみる。

ちゃんとドキュメントを追い切れていないところがあり、今回は、iovisor/bccのlibbpf-toolsにあるMakefileなどを参照したことは了解願いたい。

github.com

さて、利用するBPFプログラムは以下のようにする。 vfs_read をトレースして、読み込みに成功いたバイト数を、log2をbinとするヒストグラムにする。

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

#define MAX_ENTRIES    10240
#define MAX_SLOTS  15

// https://github.com/iovisor/bcc/blob/master/libbpf-tools/bits.bpf.h
static __always_inline u64 log2(u32 v)
{
  u32 shift, r;

  r = (v > 0xFFFF) << 4; v >>= r;
  shift = (v > 0xFF) << 3; v >>= shift; r |= shift;
  shift = (v > 0xF) << 2; v >>= shift; r |= shift;
  shift = (v > 0x3) << 1; v >>= shift; r |= shift;
  r |= (v >> 1);

  return r;
}

static __always_inline u64 log2l(u64 v)
{
  u32 hi = v >> 32;

  if (hi)
    return log2(hi) + 32;
  else
    return log2(v);
}

struct hist {
  __u32 slots[MAX_SLOTS];
};

const volatile pid_t targ_pid = -1;
static struct hist initial_hist = {0};

struct {
  __uint(type, BPF_MAP_TYPE_HASH);
  __uint(max_entries, MAX_ENTRIES);
  __type(key, u32); // pid
  __type(value, struct hist); // slots
  __uint(map_flags, BPF_F_NO_PREALLOC);
} hists SEC(".maps");

SEC("kretprobe/vfs_read")
int BPF_KRETPROBE(vfs_read, ssize_t ret)
{
  if (ret < 0)
    goto cleanup;

  u32 pid;
  u64 slot;
  struct hist *histp;

  pid = bpf_get_current_pid_tgid();
  if(targ_pid != -1 && pid != (u32)targ_pid)
    goto cleanup;

  histp = bpf_map_lookup_elem(&hists, &pid);
  if (!histp) {
    bpf_map_update_elem(&hists, &pid, &initial_hist, 0);
    histp = bpf_map_lookup_elem(&hists, &pid);
    if (!histp)
      goto cleanup;
  }

  slot = log2l(ret);
  if (slot >= MAX_SLOTS)
    slot = MAX_SLOTS - 1;
  __sync_fetch_and_add(&histp->slots[slot], 1);

cleanup:
  return 0;
}

char LICENSE[] SEC("license") = "GPL";

このプログラムを以下のようにビルドする。

# ビルドに必要な vmlinux.h は bpftool btf dump で生成。
# ref: https://facebookmicrosites.github.io/bpf/blog/2020/02/19/bpf-portability-and-co-re.html#btf
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > bpf/vmlinux.h
$ clang -g -O2 -target bpf \
   -D__TARGET_ARCH_$(uname -m | sed 's/x86_64/x86/') \
   -c bpf/helloworld.bpf.c -o bpf/helloworld.bpf.o

この helloworld.bpf.o はELF形式のバイナリで、色々とメタデータが付与されている。セクションヘッダを眺めると、 .rodata.maps のほか、BTFに関する情報が格納されている。

$ readelf -S bpf/helloworld.bpf.o
There are 24 section headers, starting at offset 0x30b0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .strtab           STRTAB           0000000000000000  00002f45
       0000000000000164  0000000000000000           0     0     1
  [ 2] .text             PROGBITS         0000000000000000  00000040
       0000000000000000  0000000000000000  AX       0     0     4
  [ 3] kretprobe/vf[...] PROGBITS         0000000000000000  00000040
       0000000000000338  0000000000000000  AX       0     0     8
  [ 4] .relkretprob[...] REL              0000000000000000  000024f0
       0000000000000050  0000000000000010          23     3     8
  [ 5] .rodata           PROGBITS         0000000000000000  00000378
       0000000000000004  0000000000000000   A       0     0     4
  [ 6] license           PROGBITS         0000000000000000  0000037c
       0000000000000004  0000000000000000  WA       0     0     1
  [ 7] .maps             PROGBITS         0000000000000000  00000380
       0000000000000028  0000000000000000  WA       0     0     8
  [ 8] .bss              NOBITS           0000000000000000  000003a8
       000000000000003c  0000000000000000  WA       0     0     4
  [ 9] .debug_loc        PROGBITS         0000000000000000  000003a8
       000000000000029b  0000000000000000           0     0     1
  [10] .debug_abbrev     PROGBITS         0000000000000000  00000643
       0000000000000197  0000000000000000           0     0     1
  [11] .debug_info       PROGBITS         0000000000000000  000007da
       00000000000004d6  0000000000000000           0     0     1
  [12] .rel.debug_info   REL              0000000000000000  00002540
       0000000000000610  0000000000000010          23    11     8
  [13] .debug_str        PROGBITS         0000000000000000  00000cb0
       000000000000024f  0000000000000001  MS       0     0     1
  [14] .BTF              PROGBITS         0000000000000000  00000eff
       00000000000007a5  0000000000000000           0     0     1
  [15] .rel.BTF          REL              0000000000000000  00002b50
       0000000000000040  0000000000000010          23    14     8
  [16] .BTF.ext          PROGBITS         0000000000000000  000016a4
       00000000000003bc  0000000000000000           0     0     1
  [17] .rel.BTF.ext      REL              0000000000000000  00002b90
       0000000000000380  0000000000000010          23    16     8
  [18] .debug_frame      PROGBITS         0000000000000000  00001a60
       0000000000000028  0000000000000000           0     0     8
  [19] .rel.debug_frame  REL              0000000000000000  00002f10
       0000000000000020  0000000000000010          23    18     8
  [20] .debug_line       PROGBITS         0000000000000000  00001a88
       0000000000000168  0000000000000000           0     0     1
  [21] .rel.debug_line   REL              0000000000000000  00002f30
       0000000000000010  0000000000000010          23    20     8
  [22] .llvm_addrsig     LOOS+0xfff4c03   0000000000000000  00002f40
       0000000000000005  0000000000000000   E      23     0     1
  [23] .symtab           SYMTAB           0000000000000000  00001bf0
       0000000000000900  0000000000000018           1    92     8

これらセクションヘッダの情報などから、ユーザランド側で使うヘッダを生成する。

bpftool gen skeleton bpf/helloworld.bpf.o > src/helloworld.bpf.h

helloworld.bpf.h はこういう内容になっている。

/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */

/* THIS FILE IS AUTOGENERATED! */
#ifndef __HELLOWORLD_BPF_SKEL_H__
#define __HELLOWORLD_BPF_SKEL_H__

#include <stdlib.h>
#include <bpf/libbpf.h>

// セクション情報などからBPFプログラムを表現する
// 構造体を生成する
struct helloworld_bpf {
    struct bpf_object_skeleton *skeleton;
    struct bpf_object *obj;
    struct {
        struct bpf_map *hists;
        struct bpf_map *rodata;
        struct bpf_map *bss;
    } maps;
    struct {
        struct bpf_program *vfs_read;
    } progs;
    struct {
        struct bpf_link *vfs_read;
    } links;
    struct helloworld_bpf__bss {
        struct hist initial_hist;
    } *bss;
    struct helloworld_bpf__rodata {
        pid_t targ_pid;
    } *rodata;
};

// 以下、helloworld_bpf構造体を操作し、BPFプログラムをアタッチするための
// ラッパ関数が並ぶ。
static void
helloworld_bpf__destroy(struct helloworld_bpf *obj)
{
    if (!obj)
        return;
    if (obj->skeleton)
        bpf_object__destroy_skeleton(obj->skeleton);
    free(obj);
}
// ...
static inline int
helloworld_bpf__create_skeleton(struct helloworld_bpf *obj)
{
    struct bpf_object_skeleton *s;

    s = (struct bpf_object_skeleton *)calloc(1, sizeof(*s));
    if (!s)
        return -1;
    obj->skeleton = s;
        // ...
        // 最後に、BPFプログラム自体がスタティックなデータとして
        // ヘッダに埋め込まれる。
    s->data_sz = 14000;
    s->data = (void *)"\
\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\
\0\0\0\0\0\0\0\0\0\0\0\xb0\x30\0\0\0\0\0\0\0\0\0\0\x40\0\0\0\0\0\x40\0\x18\0\
\x01\0\x79\x17\x50\0\0\0\0\0\xb7\x01\0\0\0\0\0\0\x6d\x71\x62\0\0\0\0\0\x85\0\0\
...\
\0\0\x09\0\0\0\0\0\0\x01\0\0\0\x5c\0\0\0\x08\0\0\0\0\0\0\0\x18\0\0\0\0\0\0\0";

    return 0;
err:
    bpf_object__destroy_skeleton(s);
    return -1;
}

#endif /* __HELLOWORLD_BPF_SKEL_H__ */

最後にこのヘッダを利用したCのプログラムを書けば、BPF CO-REなバイナリができる。

// BPFプログラムと共通のレイアウトの構造体
#include <asm/types.h>
#define MAX_SLOTS  15
struct hist {
  __u32 slots[MAX_SLOTS];
};

#include "helloworld.bpf.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>

// libbpf処理内部のログを出すための関数。
int libbpf_print_fn(enum libbpf_print_level level,
        const char *format, va_list args)
{
  if (level == LIBBPF_DEBUG)
    return 0;
  return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
  libbpf_set_print(libbpf_print_fn);

  int err, i;
  struct helloworld_bpf *obj = helloworld_bpf__open();
  if (!obj) {
    fprintf(stderr, "failed to open BPF object\n");
    exit(1);
  }

  if(argc > 1)
    obj->rodata->targ_pid = (pid_t)strtol(argv[1], NULL, 10);

  err = helloworld_bpf__load(obj);
  if (err) {
    fprintf(stderr, "failed to load BPF programs\n");
    goto cleanup;
  }

  err = helloworld_bpf__attach(obj);
  if (err) {
    fprintf(stderr, "failed to attach BPF programs\n");
    goto cleanup;
  }

  printf("Tracing vfs_read in 3 secs.\n");
  for (i = 0; i < 3; i++) {
    printf(".\n");
    sleep(1);
  }
  printf("----\n");

  {
    struct bpf_map *hists = obj->maps.hists;
    int fd = bpf_map__fd(hists);
    __u32 lookup_key = -2, next_key;
    struct hist h;

    while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) {
      err = bpf_map_lookup_elem(fd, &next_key, &h);
      if (err < 0) {
        fprintf(stderr, "failed to lookup hist: %d\n", err);
        return -1;
      }
      printf("pid: %d\n", next_key);
      for(i = 0; i < MAX_SLOTS; i++)
        printf("\tvalue: hist[%d] = %d\n", i, h.slots[i]);

      lookup_key = next_key;
    }
  }

 cleanup:
  helloworld_bpf__destroy(obj);
  return err != 0;
}

ビルドはただのCプログラムと同様。

$ gcc src/hello.c -o hello.out -l bpf
$ ls -lh hello.out 
-rwxr-xr-x 1 vagrant vagrant 30K Mar 15 11:08 hello.out

実験してみる。以下のように偏った値のreadをプログラムから発行させる。

$ ruby -e 'puts $$; f = open("/dev/urandom");
    loop { f.read([2 << 3, 2 << 6, 2 << 10].sample) }'
239576

このPIDを先程のツールにトレースさせると、確かに偏ったバイト数が連続でreadされ続けているらしいとわかる。

$ sudo ./hello.out 239576
Tracing vfs_read in 3 secs.
.
.
.
----
pid: 239576
        value: hist[0] = 0
        value: hist[1] = 0
        value: hist[2] = 0
        value: hist[3] = 0
        value: hist[4] = 196622
        value: hist[5] = 0
        value: hist[6] = 0
        value: hist[7] = 196582
        value: hist[8] = 0
        value: hist[9] = 0
        value: hist[10] = 0
        value: hist[11] = 197303
        value: hist[12] = 0
        value: hist[13] = 0
        value: hist[14] = 0

C言語でのBPF CO-REバイナリのビルド手順を、なるべく細かく記録した。

このヘッダを利用すればたとえば mrubyでもBPF CO-REのワンバイナリツールが作れる と思うので、RbBCCの夢が再びじゃんという感じなのだが、いかんせんlibbpfをmrubyから丁寧にラップするところからなので、大変そう...。今度頑張ろうと思います。