CでeBPFプログラムを開発してipコマンドで使ってみる

Vagrantの “ubuntu/groovy64” からeBPFのビルド環境を準備しました。 また実際にXDPプログラムタイプのeBPFバイトコードをビルドして使ってみました。 生成したバイトコードは ip コマンドでネットワークデバイスに設定しています。

サンプルコードはつぎの2つの記事を参考にしています。というより、このサンプルコードをビルドし動かことを目指しました。

環境を作る

Ubuntu 20.10 (Groovy Gorilla)で作ることに決めました。

1
2
3
4
5
$ cat /etc/issue
Ubuntu 20.10 /n /l

$ uname -a
Linux test-k3s 5.8.0-29-generic #31-Ubuntu SMP Fri Nov 6 12:37:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

今回必要な最低限のパッケージインストール

今回はbpfをターゲットにclangでコンパイルして ip コマンド経由でロードします。そのため最小のパッケージとして次のインストールが必要になります。

1
2
$ sudo apt-get update
$ sudo apt-get install llvm clang

目標にするeBPFプログラムはつぎになります。 単純にパケットをドロップしています。ラインセンスはGPLにしないとLinuxには読み込めません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// SPDX-License-Identifier:GPL-2.0

#include <uapi/linux/bpf.h>
// #include "bpf/bpf_helpers.h"
#define SEC(NAME) \
    __attribute__((section(NAME), used))

SEC("prog")
int xdp_dummy(struct xdp_md *ctx)
{
    return XDP_DROP;
}

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

参考にした赤帽ブログとの違いは3つあります。

  • #define KBUILD_MODNAME “xdp_dummy” を取り除いたところ
  • SEC() で指定しているセグメント名を prog に変えたところ
  • bpf/bpf_helpers.h のインクルードをやめて直接マクロを定義したところ

<uapi/linux/bpf.h>linux-source-5.8.0linux-headers-5.8.0 パッケージに含まれています。 このヘッダファイルでbpfプログラムタイプやbpfバイトコードといったものが定義されています。

どちらのパッケージも /usr/src/ 配下にファイルが置かれます。 headers はヘッダファイルが置かれるので Makefile や環境変数でパスを指定して使います。 source のではアーカイブファイルが置かれるので好きなところにコピーして展開して使います。

コメントアウトした bpf/bpf_helpers.h の方はカーネルのソースツリーの tools 配下にありLinuxのソースパッケージには含まれていません。 このインクルードファイルを有効にするには libbpf-dev パッケージをインストールするかLinuxソースコードの tools/bpf をビルドするかが必要になります。 今回このヘッダファイル内で実際に使っているのは SEC マクロだけです。 この定義は __attribute__ を使いシンボルの格納先セグメントを指定してるだけでなので自分で定義してしまいました。実際にLinux Observability with BPFでは自分で定義するサンプルがたくさん出てきます。

bpf/bpf_helpers.h をdebパッケージでインストールしたい場合はつぎのコマンドを実行してください。

1
$ sudo apt-get install libbpf-dev

またソースコードから使いたい場合は一度、 tools/bpf 配下のビルドが必要です。というのも bpf/bpf_helpers.h にインクルードされる bpf_helper_defs.h というヘッダファイルが自動生成されるファイルなのでソースツリーに含まれていないからです。

この tools/bpf のビルドに必要なパッケージはしたでインストールできます。

1
2
3
4
5
6
7
8
# カーネルビルドで使うパッケージ群
$ sudo apt-get install /
    gawk wget git diffstat unzip texinfo /
    gcc-multilib chrpath socat xterm lzop kmod libsdl1.2-dev /
    build-essential flex libelf-dev libncurses5-dev

# tools/bpfのビルドで必要なパッケージ
$ sudo apt-get install bison flex libelf1 binutils-dev libreadline-dev

実際にはここを参考解決しました。記事とはUbuntuのバージョンが違うため libelf1 と少し違う名前のパッケージをインストールしています。

ビルドついでにインストールもしておきます。BPFToolという便利ツールが手に入ります。

1
2
3
$ cd ./groovy/tools/bpf
$ make
$ make install

ビルドに使ったMakefileはこちら。これも元の例から色々と削っている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
KDIR ?= /usr/src/linux-headers-`uname -r`
CLANG ?= clang
LLC ?= llc
ARCH := $(subst x86_64,x86,$(shell arch))

BIN := xdp_dummy.o
CLANG_FLAGS = -I. -I$(KDIR)/arch/$(ARCH)/include \
              -I$(KDIR)/arch/$(ARCH)/include/generated \
              -I$(KDIR)/include \
              -I$(KDIR)/arch/$(ARCH)/include/uapi \
              -I$(KDIR)/arch/$(ARCH)/include/generated/uapi \
              -I$(KDIR)/include/uapi \
              -I$(KDIR)/include/generated/uapi \
              -I$(KDIR)/include/linux/kconfig.h \
              -I$(KDIR)/tools/testing/selftests/bpf/ \
              -O2 -emit-llvm

all: $(BIN)

xdp_dummy.o: xdp_dummy.c
        $(CLANG) $(CLANG_FLAGS) -c $< -o - | \
                $(LLC) -march=bpf -mcpu=$(CPU) -filetype=obj -o $@

使ってみる

clangでビルドしてipコマンドでデバイスに設定してみます。どのデバイスに設定するかは慎重に確認してください。パケットを全てDROPするので通信できなくて再起動の道しかなくなります。

1
2
3
4
5
$ make
# BPFプログラムを設定する
$ sudo ip link set dev enp0s8 xdp object ./xdp_dummy.o
# BPFプログラムを外す
$ sudo ip link set dev enp0s8 xdp off

動画にしてyoutubeに上げました。k3sの実験に使っていた仮想マシンを使いまわしたのでホスト名が無関係なのは気にしないでください。 また時間はUTCです。クリスマスイブの夜に動画を作りました。

動画とは別のときですがpingは下のような結果になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$ ping 192.168.31.34
PING 192.168.31.34 (192.168.31.34): 56 data bytes
64 bytes from 192.168.31.34: icmp_seq=0 ttl=64 time=0.725 ms
64 bytes from 192.168.31.34: icmp_seq=1 ttl=64 time=0.299 ms
64 bytes from 192.168.31.34: icmp_seq=2 ttl=64 time=0.434 ms
64 bytes from 192.168.31.34: icmp_seq=3 ttl=64 time=0.436 ms
64 bytes from 192.168.31.34: icmp_seq=4 ttl=64 time=0.364 ms
64 bytes from 192.168.31.34: icmp_seq=5 ttl=64 time=0.610 ms
64 bytes from 192.168.31.34: icmp_seq=6 ttl=64 time=0.835 ms
64 bytes from 192.168.31.34: icmp_seq=7 ttl=64 time=0.415 ms
64 bytes from 192.168.31.34: icmp_seq=8 ttl=64 time=0.521 ms
64 bytes from 192.168.31.34: icmp_seq=9 ttl=64 time=0.487 ms
64 bytes from 192.168.31.34: icmp_seq=10 ttl=64 time=0.269 ms
64 bytes from 192.168.31.34: icmp_seq=11 ttl=64 time=0.344 ms
64 bytes from 192.168.31.34: icmp_seq=12 ttl=64 time=0.460 ms
64 bytes from 192.168.31.34: icmp_seq=13 ttl=64 time=0.245 ms
64 bytes from 192.168.31.34: icmp_seq=14 ttl=64 time=0.301 ms
64 bytes from 192.168.31.34: icmp_seq=15 ttl=64 time=0.328 ms
64 bytes from 192.168.31.34: icmp_seq=16 ttl=64 time=0.692 ms
64 bytes from 192.168.31.34: icmp_seq=17 ttl=64 time=0.494 ms
64 bytes from 192.168.31.34: icmp_seq=18 ttl=64 time=0.329 ms
64 bytes from 192.168.31.34: icmp_seq=19 ttl=64 time=0.316 ms
64 bytes from 192.168.31.34: icmp_seq=20 ttl=64 time=0.340 ms
64 bytes from 192.168.31.34: icmp_seq=21 ttl=64 time=6.067 ms
64 bytes from 192.168.31.34: icmp_seq=22 ttl=64 time=0.732 ms
64 bytes from 192.168.31.34: icmp_seq=23 ttl=64 time=0.565 ms
64 bytes from 192.168.31.34: icmp_seq=24 ttl=64 time=0.560 ms
Request timeout for icmp_seq 25
Request timeout for icmp_seq 26
Request timeout for icmp_seq 27
Request timeout for icmp_seq 28
Request timeout for icmp_seq 29
Request timeout for icmp_seq 30
64 bytes from 192.168.31.34: icmp_seq=31 ttl=64 time=0.706 ms
64 bytes from 192.168.31.34: icmp_seq=32 ttl=64 time=0.398 ms
64 bytes from 192.168.31.34: icmp_seq=33 ttl=64 time=0.777 ms
Request timeout for icmp_seq 34
Request timeout for icmp_seq 35
Request timeout for icmp_seq 36
Request timeout for icmp_seq 37
64 bytes from 192.168.31.34: icmp_seq=38 ttl=64 time=0.251 ms
64 bytes from 192.168.31.34: icmp_seq=39 ttl=64 time=0.404 ms
64 bytes from 192.168.31.34: icmp_seq=40 ttl=64 time=0.976 ms
64 bytes from 192.168.31.34: icmp_seq=41 ttl=64 time=1.775 ms
^C
--- 192.168.31.34 ping statistics ---
42 packets transmitted, 32 packets received, 23.8% packet loss
round-trip min/avg/max/stddev = 0.245/0.702/6.067/1.006 ms

よりみち環境構築

GCCでのコンパイルなど

今回はBPFをターゲットにコンパイルする必要最小限でインストールしました。BPF関連のパッケージはいろいろあるので紹介しておきます。

GCCでもBPFをコンパイルできるようです。というか元々はGCCのみサポートしていたみたいです。 ただし私はリンクに失敗してeBPFとしてローダブルなELFファイルを作れていません。

GCCでBPFをコンパイルするには下のパッケージインストールが必要なようです。

1
$ sudo apt-get install gcc-bpf

BPF Compiler Collection

BPFをターゲットにコンパイルはできますがロードしたり適切なイベントにアタッチしたりと他にも大変なことがあります。 ちゃんとしたプログラムを作るためのBPF Compiler Collection(BCC)と呼ばれるツールキットがあります。 これは動的にCコードをeBPFバイトコードにコンパイルしたり、カーネルに読み込んだり必要なBPFマップやイベントと紐付けたりする機能を持っています。ライブラリとヘッダとで2つのパッケージがあります。

1
2
$ sudo apt-get install \
    libbpfcc libbpfcc-dev

このBCCを使って開発されたツール群やPython3, Luaのラッパーも提供されていて次のようにインストールできます。

1
2
$ sudo apt-get install \
    bpfcc-tools python3-bpfcc bpfcc-lua

すこし変わりますが、iovisor/gobpfというBCCのGoバインディングもあります。

bpftrace

DSLでeBPFのトレース機能を使えるツールも提供されています。パッケージに *.bt という名前でトレーススクリプトがたくさんあるので勉強するときはこれらを参考にするのが良さそうです。

1
2
3
4
5
$ sudo apt-get install bpftrace
$ dpkg -L bpftrace | grep -E 'bin/.*\.bt$'
...
/usr/sbin/syscount.bt
...

systemtap

System Tapのスタティックプローブにアタッチするのに下のパッケージのインストールが必要なようです。

1
$ sudo apt-get install systemtap-sdt-dev

おわりに

これでBPFを使う入り口にたどり着けたと思っています。 eBPFのまとまった入門はLinux Observability with BPFBPF Documentation — The Linux Kernel documentationの通読くらいにして、そろそろXDPを使った実際のプロジェクトを何か決めて読んだりバグフィックスなどに取り組めたらと思います。 ただ周辺としてバイナリツールやリンカそしてQEMUとPackerは少し確認するつもりです。またDPDK、P4などよく上がる比較対象については簡単に説明できる程度には知っておかないとなぁと思ってます。いろいろと広くて楽しい。

また、今回のよりみちも含めた環境はしたの Vagrantfile から作れます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Vagrant.configure("2") do |config|
  config.vm.define "dev-bpf" do |host|
    host.vm.box = "ubuntu/groovy64"
    host.vm.hostname = "dev-bpf"
    host.disksize.size = '20GB'

    host.vm.network "private_network", ip: "172.16.0.10"

    host.vm.provider "virtualbox" do |v|
      v.customize ["modifyvm", :id, "--uart1", "0x3f8", "4"]
      # ref. https://www.virtualbox.org/manual/ch03.html#serialports
      v.customize ["modifyvm", :id, "--uartmode1", "file", "/tmp/#{host.vm.hostname}_com"]
      v.memory = 4096
    end
    host.vm.provision "shell", inline: <<-SHELL
      apt-get update

      # libbpfを使うビルド
      apt-get install llvm clang gcc-bpf
      # libbpfに向けたヘルパー
      apt-get install libbpf-dev
      # カーネルビルド
      apt-get install \
        gawk wget git diffstat unzip texinfo \
        gcc-multilib chrpath socat xterm \
        lzop kmod libsdl1.2-dev \
        build-essential flex \
        libelf-dev libncurses5-dev
      # tools/bpfビルド
      apt-get install \
        bison flex libelf1 binutils-dev \
        libreadline-dev
      # SystemTapのスタティックプローブ利用
      apt-get install systemtap-sdt-dev
      # BCC利用
      apt-get install libbpfcc libbpfcc-dev
      # BCCのラッパーや利用ツール
      apt-get install bpfcc-tools \
        python3-bpfcc bpfcc-lua
      # bpftrace
      apt-get install bpftrace
    SHELL
  end
end
comments powered by Disqus