bpftrace 2020

(この記事はLinux Advent Calendar 2020 - Qiitaの2日目の記事です.前日はmasami256さんのTiny Core LinuxでLinuxのinitプロセスが実行されるあたりを調べるでした.)


みなさんこんにちはこんばんは.bpftraceを使っていますか? (e)BPFといえば最近は専らCiliumなどネットワークが話題ですが,bpftraceはBPF*1を利用したトレーシングツールです.私は昨年からぼちぼち使い始めて,機能追加やバグ修正のパッチをいろいろと投げていた(130+)ら気づいたらコミッタになっていました.ということでbpftraceについては比較的よく知っていると思うので,今年にbpftraceに追加された主要な機能や変更点を紹介したいと思います.

bpftrace知らないなぁという方はこちら(拙文です)を,bpftrace使ってないなぁという方は折角ですから最後の小話だけでも読んでみてはいかがでしょうか.また最近bpftraceをmacから簡単に試せるものを作ったので,macユーザでbpftraceに興味のある方はそちらもどうぞ(勿論M1は対象外です..).

目次

本題に入る前に

bpftraceはこの一年間で多くの機能追加,バグ修正がおこなわれました.bpftraceをお使いの方は今一度アップデートの確認をおすすめします.もし以下で紹介する機能が使えない場合,bpftraceのバージョンが古い可能性が高いです.また一部機能はカーネルバージョン(主にカーネル5.6以上)や,libbpfと一緒にbpftraceをコンパイルする必要があります(特にBTFがそれに該当します).

bpftraceのバージョンやサポートしている機能の確認

bpftraceのバージョンや,サポートしている機能は,bpftrace --info コマンドから確認できます.

$ sudo bpftrace --info
System
  OS: Linux 5.9.0+ #26 SMP Mon Nov 16 11:50:54 JST 2020
  Arch: x86_64

Build
  version: v0.11.0-287-gc62f
  LLVM: 10.0.1
  foreach_sym: yes
  unsafe uprobe: no
  bfd: yes
  bpf_attach_kfunc: yes
  bcc_usdt_addsem: yes
  bcc bpf_attach_uprobe refcount: no
  libbpf: yes
  libbpf btf dump: yes
  libbpf btf dump type decl: yes

Kernel helpers
  probe_read: yes
  probe_read_str: yes
  probe_read_user: yes
  probe_read_user_str: yes
  probe_read_kernel: yes
  probe_read_kernel_str: yes
  get_current_cgroup_id: yes
  send_signal: yes
  override_return: yes
  get_boot_ns: yes
  dpath: no

Kernel features
  Instruction limit: 1000000
  Loop support: yes
  btf (depends on Build:libbpf): yes
  map batch (depends on Build:libbpf): yes
  uprobe refcount (depends on Build:bcc bpf_attach_uprobe refcount): no

Map types
  hash: yes
  percpu hash: yes
  array: yes
  percpu array: yes
  stack_trace: yes
  perf_event_array: yes

Probe types
  kprobe: yes
  tracepoint: yes
  perf_event: yes
  kfunc: yes

BTFサポートの強化

それでは早速追加された主要な機能を振り返っていきたいと思います.今年はなんといってもBTF (BPF Type Format) を利用した機能がいろいろと追加されました.以下ではどんなことができるか簡単に紹介していきます.

そもそもBTFとは

BTFにはいろいろな用途があるため,簡単に説明するのは難しいのですが,bpftraceの文脈でいうと/proc/kallsyms が現在動いているカーネルシンボルのアドレスを提供するのと同じように,BTFは現在動いているカーネルが利用している関数やデータ構造の情報を提供します.例えば,BTFを参照すればそのカーネルが利用している,プロセスの状態を保持する struct task_struct の構造を知ることができ,どのメンバにアクセスするにはどれだけのオフセットが必要かが分かります.他には例えば,vfs_open() 関数の引数の数や引数の型が分かります.これで何が嬉しいの? と思うかもしれませんが,カーネル内の関数や構造体はunstableで常に変化します.カーネルトレーシングをする場合,多くの場合はカーネルの関数や構造体にアクセスするため,関数や構造体が変化してしまうと折角書いたトレーシングスクリプトが特定のカーネルでしか動かないということになります.BTFによりそのカーネルが利用している関数やカーネルが分かれば,その情報を利用してスクリプトを補正することで,複数カーネルで動作するトレーシングスクリプトが動作できるようになるというわけです.鋭い方はそれってDWARFでできるのでは?と思うことでしょう.勿論DWARFでも可能ですが,BTFはトレーシング用途に特化してプロダクション環境でも利用できることを一つの目標にしているため,サイズが非常に小さいです(数MB程度).ただしその代わりDWARFと比べて当然情報量が少ないため,できることは必然的に限られます.

bpftraceは実行時にトレーシングスクリプトコンパイルする形です.BTFが利用可能な場合は,bpftraceはコンパイル時にBTF情報を利用して,適切なオフセットでデータにアクセスするBPFプログラムを生成します(もしBTFが利用できない場合は,includeしたヘッダに含まれる定義情報を利用します).一方で,実行前にトレースプログラムをあるカーネルバージョン上でコンパイルしておき,BTFを利用してそのトレースプログラムをロードする前に,データ構造アクセスのオフセットの修正をおこなうという方法もあります.こちらは BPF CO-REと呼ばれる機能で,こちらはこちらで非常に重要で積極的に開発が進められていますが(特にiovisor/bccの方で),bpftraceとは直接は関係ありません.

BTFカーネルの関数や構造体の情報を提供するだけのものではありません.BTFについてより詳しくはこの辺り(拙文です)カーネルのドキュメントをご参照ください.

BTFを使用するには

BTFの機能を利用するには,CONFIG_DEBUG_INFO_BTF を有効にしたカーネルが必要です.またカーネル5.6以上でないと以下で説明するkfuncの機能は使えません.bpftrace自体もlibbpfと一緒にコンパイルする必要があります. 

bpftrace --infoBTFの機能が利用できるか確認できます.特に,

  • BTFを使いたい場合は "Kernel features" のbtf が yes
  • kfuncを使いたい場合は Probe typesの kfunc がyes

である必要あります.

BTFでできること

それではBTFでできることをみていきましょう.

カーネル構造体の自動解決

例として,vfs_open(struct path*, strust file *) 関数の呼び出しをフックして,vfs_openが呼ばれた時のpath のファイル名を呼び出すスクリプトを考えます(これはbptraceのreference guideからの引用です).BTFが無い以前は,スクリプトが利用する struct path や. struct dentry が定義してあるヘッダをインクルードする必要がありました.

#include <linux/path.h>
#include <linux/dcache.h>

kprobe:vfs_open
{
    printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name));
}

もしBTFが使用できる環境であれば,以下のようにヘッダをインクルードしなくても,スクリプト側で利用するカーネル構造体が,特に何もしなくても利用できるようになります.

kprobe:vfs_open
{
    printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name));
}

内部的にはまずBTFから struct path の情報を取ってきて,そこからスクリプトがアクセスする構造体情報を集めてきてそれを利用しています.

kfuncの利用

上記のスクリプト例ではkprobeを利用していました.kprobeは基本的にカーネル内のどのアドレスにもアタッチできるの非常に便利ですが,一方でbpftraceでは関数にアタッチしたとき,自分で引数をキャストする必要(上記の例だと (struct path *)arg0) がありました.)カーネル5.6から,kfuncという,kprobeとはまた違うトレース方法が追加されました.kfuncはkprobeと違い関数のentryまたはexitにしかアタッチできませんが,BTFの情報を利用することで,以下のように構造体のキャストなしに関数の引数にアクセスできます.

kfunc:vfs_open
{
    printf("open path: %s\n", str(args->path->dentry->d_name.name));
}

ポイントは args->path で,これにより vfs_open() の引数 path にアクセスしています.何故これができるかというと,BTFには関数の情報も入っているので,まず vfs_open の関数のプロトタイプ情報をとてきて,それから args->pathvfs_open のpathにアクセスしてることが分かるので,そこからスクリプトが生成できるというわけです. またkfuncの場合kprobeと違って引数が自動でキャストされるので,間違ったキャストをしてしまうというミスもなくなります.

カーネル関数の引数や構造体の確認

ややおまけ的機能ですが,以下のようにしてBTFを利用して関数のプロトタイプや,構造体のメンバが確認できます.

% sudo bpftrace -lv "kfunc:vfs_open"
kfunc:vfs_open
    const struct path * path;
    struct file * file;
% sudo ./src/bpftrace -lv "struct path"
struct path {
        struct vfsmount *mnt;
        struct dentry *dentry;
};

この構造体のメンバなんだっけ?となったときカーネルソースを確認する必要がなくなります.

現状のBTFの制約

BTFは非常に便利ですが,以下のような制約・課題があります.

  • 現状BTFに含まれる関数はnon-statitcなもののみ (kprobeはstaticな関数にも対応しています)
  • カーネルモジュールに未対応 (もしカーネルモジュールをBTFでトレーシングしたい場合は,カーネル側にモジュールえはなく組み込んでビルドする必要があります)
  • define 値の欠如

この辺りの問題はいろいろと解決に向けて議論がされています.特に最後のdefineされている値が欠如されているというのは地味にトレースプログラムを書く時に障害になります.例えば AF_INET の値を知りたいときは,BTFがあってもそれがdefineされているヘッダをインクルードする必要があるわけです.解決策の一つは#defineではなくenumを使うことで (enumの情報はBTFに含まれます),実は一部ヘッダはBTFに対応できるようにそのような変更が加えられています.将来的にはこの辺りの問題が解決されると嬉しいですね.

Docker ビルド・スタティックビルドの提供

x86_64用ですが,ホストをトレースするためのDockerビルドがあります.以下のようにして利用できます.

% sudo docker run -it --privileged -v /sys/kernel/debug:/sys/kernel/debug:rw -v /lib/modules/:/lib/modules:ro  -v /usr/src:/usr/src:ro quay.io/iovisor/bpftrace tcpconnect.bt

適切にdebugfsやカーネルヘッダの場所をコンテナ側にマウントする必要がありますが,docker buildを使うとbpftraceの最新版が使えて便利です.ただ残念ながら現状BTFのサポート(というかlibbpfとのビルド)はありません.

また,github actionによりスタティックビルドが提供されるようになっています.Releaseからリリースバージョンのスタティックビルドが取得できる他,やや分かりにくいですがgithubアクションのembedded buildの項目から最新のスタティックビルドが取得できます.

知っておくと便利な機能

タプル

以下のようにタプルが使えるようになりました.わざわざ複数マップを作らなくても値が格納できます.

% bpftrace -e 'BEGIN { @ = (0, 1, "abc"); printf("%d\n", @.0); print(@); }'
Attaching 1 probe...
0
@: (0, 1, abc)

マップの複数キーの利用

別にこれ自体は前からあった機能なのですが,いまいち認知されていないようなので書いておきます.以下のように @[] に使うキーは複数個指定できます.

% sudo ./share/bpftrace -e 'BEGIN { @[pid, cpu] = 1; print(@); }'
Attaching 1 probe...
@[10189, 35]: 1

これを使うと条件(例えばCPU番号)に応じた集計などが簡単にできます.

バイナリ値のダンプ

buf() を使うと,ポインタ先のデータをダンプができます.似たような関数に str() がありますが, str() はNULL終端された文字列を扱う点が異なります.

% bpftrace -e 'tracepoint:syscalls:sys_enter_sendto
    { printf("Datagram bytes: %r\n", buf(args->buff, args->len)); }' -c 'ping 8.8.8.8 -c1'
Attaching 1 probe...
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
Datagram bytes: \x08\x00@\xc3\x05\xb6\x00\x01\xe9\x0aW_\x00\x00\x00\x00\xb0H\x02\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x1
6\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&'()*+,-./01234567
^C
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

エラーチェック

-k あるいは -kk オプションでBPFプログラム実行中のエラーをチェックするようになります.

% bpftrace -kk -e 'BEGIN { @[0] = 1; @[1]++; }'
Attaching 1 probe...
stdin:1:19-25: WARNING: Failed to map_lookup_elem: 0
BEGIN { @[0] = 0; @[1]++;}
                  ~~~~~~

もしbpftraceのスクリプトが期待したとおりに動かない場合,とりあえず -kk をつけて実行してみると良いと思います.

この機能について補足すると,BPFプログラムにはヘルパー関数と呼ばれる関数を利用して,カーネルの処理を呼び出す機能があります.これはいわばユーザプログラムにおけるシステムコールのようなもので,BPFマップの操作や出力処理などの機能は全てこのヘルパー関数によって成り立っています.bpftraceを利用する場合は特にこのヘルパー関数を意識する必要はないですが,重要なのはヘルパー関数の処理は何らかの理由によって失敗することがあるということです.このとき,ヘルパー関数は戻り値としてエラーを返しますが,デフォルトではこのエラー値は無視されます.これをチェックするのが -k および -kk オプションです. -k-kk の違いは,チェックするヘルパー関数の数です.bpftraceはデータの読み出しをおこなうヘルパー関数が失敗すると,値を0として返すという仕様になっています.これによって,例えば / @[tid] / のように書くと, @[tid] が存在しない場合はその処理がスキップできることになります.このようなテクニックはbpftraceのスクリプトで広く使われていて,このエラーをチェックしてしまうと冗長な場合があるため, -k オプションではこのようなデータ読み出しの関数に関してエラーはチェックしません. -kk だと全てのヘルパー関数のエラーをチェックします.一つ注意点として,エラーチェック機能を有効にするとBPFプログラムがその分長くなります.場合によってはそのオーバヘッドが問題になる可能性があります.また,ロードできるBPFプログラム長には制限があるため,大きなbpftraceスクリプトではエラーチェック機能が利用できない可能性があります.

whileループ

もともとBPFプログラムは停止性保証のためにループ(後方ジャンプ)が一切許可されていませんでしたが,verifierが賢くなって有限回で停止するループ処理が書けるようになりました.カーネル5.3以上が必要です.bpftraceでは while() を使います.

% bpftrace -e 'i:ms:100 { $i = 0; while ($i <= 100) { printf("%d ", $i); $i++} exit(); }'

ここで,verifierは実際にループ処理を一つ一つ解釈していき,そのループが停止するかをみています.従って,極端に大きなループはverifierの命令解釈数上限に引っかかるため実行できません.例えば,以下のプログラムはエラーになります.

$i = 0; while ($i <= rand) { printf("%d ", $i); $i++} exit(); }

これは rand はuint64の値を返す関数で,verifierはその値の最大を想定してverificationをおこなうためです.以下のように,明示的にループ上限を定めてあげるとプログラムが実行できるようになります.

$i = 0; $max = rand; if ($max > 100) { $max = 100;}
while ($i <= $max) { printf("%d ", $i); $i++} exit();}

その他主要な変更・更新点

s390x, aarch64のサポート

ビッグエンディアンアーキテクチャに存在した一部問題が修正されました.またaarch64などのアーキテクチャでは,カーネルモードとユーザモードのアドレススペースが異なることに起因して,それぞれのデータの読み出しに専用のヘルパー関数*2を利用する必要があり,しばらくの間bpftraceはこれをサポートしていませんでしたが,つい最近この機能がサポートされました.結果としてaarch64でも問題なくbpftraceが使えるようになっているはずです.

日本語版ドキュメント

ワンライナーチュートリアルには日本語訳(拙訳です)があります.Reference Guideは英語のみです.

Discourse フォーラム

Discourceにbpftraceのフォーラムができました.質問はここに投げると良いと思います.開発者の方が見ています.

より詳しく知りたい方

実際には他にもいろいろと機能が追加されています.詳しくは CHANGELOG をどうぞ.

バグ情報

最後に,現在のbpftraceには printf()したときに間違った値がごくまれに表示される という割と致命的なバグがあります.長い間原因が分からず開発者を困らせていまいしたが,どうやらLLVMのバックエンドに問題があるということが分かりました(詳しくは #1305, Bugzilla – Bug 47591 をどうぞ).2020年12月現在このバグは修正されておらず,現状これに対応する完璧な方法はないのですが,printf()の位置を変えたりすると結果的にBPFのコードが変わるので問題が解決する場合があります.上記で紹介した -k あるいは -kk オプションも有効です.もしbpftraceを使っていて明らかに出力がおかしい場合はこのバグを疑ってみてください.ちなみに本当にこのバグかどうかはBPFプログラムのアセンブラコードを眺めてみると分かります.不自然にloadとstore命令が入れ替わっている箇所があるはずです.

2020-12-4 追記

このバグですが,LLVM12で修正されたそうです.ただしLLVM12が実際に利用されるようになるのは数年かかるでしょうし,bpftrace側でもなんとか修正できないかいろいろと検討しています.

追記ここまで

おまけ

bpftaceによるBitVisorのトレース

先日BitVisorをbpftraceトレースするという内容の発表をBitVisor Summit 9でおこないました.言ってる意味が不明かもしれませんが,興味のある方はご笑覧ください ⇒ スライド

macからbpftraceを試す

最初に書きましたが,Intel macからbpftraceを簡単に試せるものを作りました.詳しくはこちらをどうぞ

システム系論文の情報収集方法

あまりBPFと関係なくて恐縮ですが,昨日システム系論文の情報収集方法というエントリを書きました.学術的にもBPFを使った論文は結構出ているので,気になる方はチェックしてみてください.

最後に-- bpftraceに関わってきて

最後に折角の機会なので一年間bpftraceに関わってきた感想を書いてみようと思います.まず.bpftraceはもともとAlastair Robertsonさん一人によって2017年から開発されたプロジェクトです(すごい).その後,Brendan Greggさんにより多くのツールが追加され,さらにNetflixのエンジニアの方によって基本的な機能の多くが追加されました.だいたい去年の春先ごろのことだと思います.書籍"BPF Performance Tools" (Brendan Gregg箸)はその辺のバージョンのbpftraceをもとに書かれています.そのころと比べて,bpftraceは結構進化していると思います.現在は前述の3人はコミットに関しては控えめで,今は私を含めた別の3人が主にプロジェクトに関わっています.特にその中の一人のFBのエンジニアの方がメインのメンテナといった感じです.その他Red HatIBMの方からも定期的にコントリビューションがあります.

これまでに私がコミットした主な内容には以下があります.

  • 整数型ポインタのキャストのサポート#942
  • kprobe offset に対するアタッチ機能の追加 #956
  • BPFコンテキストアクセスの修正 #1104
  • bpftrace oneliner ドキュメント翻訳 #1176, 訳文
  • bpf helper functionエラーのレポート機能 #1276
  • fuzzingのサポート #1601, #1633, ドキュメント
  • その他のバグ修正など

一番最初のPRは整数型ポインタのキャストのサポート ((int8*)$var みたいなやつ) でした.これはbpftraceを使い始めてすぐにこの機能がなくて不便だったので,なんとなくソースを見て修正してPRを送ったものです.C++なんて3年以上まともに書いてなかったし,そもそもC++は理解してないし,このとき多分1割ぐらいしかbpftraceのことを分かってなかったんですが,少しフィードバックもらったあとにマージされたのがはじまりでした.bpftraceは使っていくほどいろいろと問題に遭遇したので,息抜きとして空いてる時間にそれを直して行ったら今の状態になったというのが実情です.ここまで関わったOSSプロジェクトはbpftraceが初めてですが,やはり世界中の人と作業をしていくというのはOSSの醍醐味で,とても楽しい経験です.作者の方に"good work!"と言われるとやはり嬉しいものです.

bpftraceにコミットすることでBPFに関してより詳しくなったのは勿論のこと,もともとも私はLinuxのトレーシング手法についていろいろと調査して使ってきましたが,具体的にそれらが利用されるかについての理解が深まりました.最近改めて"BPF Performance Tools"を読み返してみたのですが,ついに真に理解できるようになったような気がしています.

またbpftrace自体はflex/bisonによる字句解析,構文解析をおこなった後,LLVMによりIRを出力するコンパイラです.よくCPU, OS, コンパイラを自作すると良いなんて話がありますが,私は簡単なCPUやOSは作ったことがあったものの*3コンパイラは真面目に取り組んだことがなかったので,その辺もいろいろと勉強になりました.(と同時にLLVMの難しさも知ることに...)bpftraceはコンパクトにいろいろとまとまってると思うので,これらについて興味のある方はコードを見てみると楽しいのではないかなと思います.

来年の抱負

来年は今年と同様,本業が一番なのは勿論ですが,bpftraceに関するコミットは継続しつつ,カーネルの方に対してもコミットしていけたら...なんて思います.FBのBPFやってるエンジニアはカーネルがいじれるのは勿論のこと,LLVM(本体の方)もできるのでみんなすごいなぁと思う次第です.まだまだ自分も頑張らないといけませんね.

それではみなさん,ちょっと早いですがよいお年を.

*1:以後,BPFと言えばeBPFを指します

*2: bpf_probe_read_kern, bpf_probe_read_user

*3:といっても自作したCPUで自作したOSを動かした訳ではないのでその辺りはまだまだですね