単なるデバッグ情報だけではない「BPF Type Format」(BTF)の使い道:Berkeley Packet Filter(BPF)入門(10)
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、最近のBPFの発展に欠かすことのできない重要機能「BPF Type Format(BTF)」について。
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載「Berkeley Packet Filter(BPF)入門」。今回は、最近のBPFの発展に欠かすことのできない重要機能「BPF Type Format(BTF)」を紹介します。
BPF Type Format(BTF)とは
BPFプログラムは主にC言語で記述することが多いですが、カーネルにロードされるのはコンパイルされたバイナリデータです。「カーネルにどのようなBPFプログラムをロードしているか」ということは後から確認できますが、ただのバイナリデータだけではそのBPFプログラムが何をするのか理解するのは困難です。
一般のプログラムでは、コンパイルしたバイナリデータにそのプログラムのソースの情報やデータ構造の情報を「デバッグ情報」という形で持たせることができます。代表的なデバッグ情報のフォーマットに「DWARF」があります。DWARFを利用することで、バイナリコードに対応したソースコードの位置や関数の引数の型情報などが得られます。
BPF Type Format(BTF)は、DWARFのように、BPFプログラムのソース情報やデータ構造を保持するためのデータフォーマットです。BPFプログラムを補助するためのメタデータだと思えば分かりやすいでしょう。BTFはBPFプログラムのデバッグのみではなく、BPFのさまざまな機能実現のために利用されています。
BTFの具体的な仕様は、カーネルのドキュメントを参照してください。DWARFのデバッグ情報はサイズが大きいこともあり、一般にプログラムのデバッグをするときだけその機能を有効にすることが多いです。一方でBTFは、BPFを利用する場合は常にあること(あるいは、常にあっても問題ないこと)を想定しています。このため、BTFはBPFプログラムに特化した設計になっており、保持する情報を絞ることでそのサイズを小さくしています。
BTFの使用用途は大きく2つに分けられます。1つ目は、BPFプログラムのデバッグ情報としての利用です。そして2つ目は、Linuxカーネルのデータ構造情報を取得するための利用です。以降、それぞれについて説明します。
BPFプログラムのデバッグ情報としてのBTF
ClangにはBTFのサポートが含まれています。デバッグ情報ありでBPFプログラムをコンパイルすると、BTFが生成されます。生成されたBTFは特定のELFセクション(.BTFおよび.BTF.ext)に格納されます。例えば、下記のようにしてC言語のファイルからBTFが生成できます。
% clang -target bpf -g -c a.c % readelf -S a.o There are 16 section headers, starting at offset 0x6c0: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align ... [ 5] .BTF PROGBITS 0000000000000000 000001cb 00000000000000de 0000000000000000 0 0 1 [ 6] .BTF.ext PROGBITS 0000000000000000 000002a9 0000000000000070 0000000000000000 0 0 1 ...
また、「pahole」というツールを利用して、DWARFの情報をBTFに変換することも可能です。
こうして得られるBTFのデータをBPFプログラムのロード時やマップ作成時に指定することで、特定のBPFプログラムやBPFマップにBTFの情報をひも付けることができます。BTFの情報はBPFプログラムやマップと一緒にカーネル内に保持され、後からその情報を参照することができます。
BCCでのBTFの利用例
連載第6回で紹介した、BPFプログラム作成のためのライブラリ「BPF Compiler Collection(BCC)」には、BTFのサポートが含まれています。BTFを利用することで、コンパイルしたプログラムとソースコード情報の対応付けや、作成したマップのデータ構造の取得などが可能です。
例として、下記のBCCプログラムを考えます。
#!/usr/bin/env python import bcc text = r""" #include <linux/ptrace.h> struct data_t { u32 a; u32 b; }; BPF_HASH(hash, int, struct data_t); int func(volatile struct pt_regs *ctx) { struct data_t data = { .a = 1, .b = 2 }; int key = 0; hash.update(&key, &data); return 0; } """ b = bcc.BPF(text=text, debug=0x8) b.attach_perf_event(ev_type=bcc.PerfType.SOFTWARE, ev_config=bcc.PerfSWConfig.CPU_CLOCK, fn_name="func", sample_freq=1, cpu=0) b.trace_print()
ここで、「bcc.BPF(text=text, debug=0x8)」と、BCCでコンパイルする際に引数の「debug」に0x8を渡すと、BCCはBTFの情報をダンプします。下記に出力例を示します。
% sudo ./example.py Disassembly of section .bpf.fn.func: func: ; int func(volatile struct pt_regs *ctx) { // Line 22 0: 18 01 00 00 01 00 00 00 00 00 00 00 02 00 00 00 r1 = 8589934593 ll ; struct data_t data = { .a = 1, .b = 2 }; // Line 24 2: 7b 1a f8 ff 00 00 00 00 *(u64 *)(r10 - 8) = r1 3: b7 01 00 00 00 00 00 00 r1 = 0 ; int key = 0; // Line 25 4: 63 1a f4 ff 00 00 00 00 *(u32 *)(r10 - 12) = r1 ; bpf_map_update_elem((void *)bpf_pseudo_fd(1, -1), &key, &data, BPF_ANY); // Line 26 5: 18 11 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 ld_pseudo r1, 1, 4294967295 7: bf a2 00 00 00 00 00 00 r2 = r10 8: 07 02 00 00 f4 ff ff ff r2 += -12 9: bf a3 00 00 00 00 00 00 r3 = r10 10: 07 03 00 00 f8 ff ff ff r3 += -8 11: b7 04 00 00 00 00 00 00 r4 = 0 12: 85 00 00 00 02 00 00 00 call 2 ; return 0; // Line 27 13: b7 00 00 00 00 00 00 00 r0 = 0 14: 95 00 00 00 00 00 00 00 exit
このように、BTFのデータに基づき、BPFのバイナリとソースコードの対応が分かります。なお、上記のソースコードとBTFによる出力によるソースコードが一部違うのは、BCCがプログラムをロードする際にソースコードの一部を書き換えたためです。
これだけでは少しありがたみが分かりにくいかもしれません。別の例として、下記のようにプログラムを一部書き換えて実行しています。
int func(volatile struct pt_regs *ctx) { struct data_t data = { .a = 1, .b = 2 }; int key = 0; hash.update(&ctx->ax, &data); // &key から &ctx->ax に変更 return 0; }
すると、下記のようなエラーが得られます。
% sudo ./example.py [...] bpf: Failed to load program: Permission denied Unrecognized arg#0 type PTR ; int func(volatile struct pt_regs *ctx) { 0: (bf) r2 = r1 1: (18) r1 = 0x200000001 ; struct data_t data = { .a = 1, .b = 2 }; 3: (7b) *(u64 *)(r10 -8) = r1 ; bpf_map_update_elem((void *)bpf_pseudo_fd(1, -1), &ctx->ax, &data, BPF_ANY); 4: (18) r1 = 0xffff9e28933be800 ; bpf_map_update_elem((void *)bpf_pseudo_fd(1, -1), &ctx->ax, &data, BPF_ANY); 6: (07) r2 += 80 7: (bf) r3 = r10 ; 8: (07) r3 += -8 ; bpf_map_update_elem((void *)bpf_pseudo_fd(1, -1), &ctx->ax, &data, BPF_ANY); 9: (b7) r4 = 0 10: (85) call bpf_map_update_elem#2 R2 type=ctx expected=fp processed 9 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0 [...]
ここでは、「bpf_map_update_elem()」のkeyの引数はスタック上の変数でなければいけないため、エラーになっています。BPFの検証器のエラーメッセージは、そのままでは分かりにくいものが多いですが、今回のように「どのソースの箇所が問題か」が分かるだけで、デバッグしやすくなります。
ロードしたプログラムやマップのデータ構造の確認
bpftoolを利用することで、カーネルにロードしたBPFの情報を取得することができます。上述のBCCのプログラムを実行した状態で、以下のようにbpftoolでロードしたプログラムを確認できます。
% sudo bpftool prog [...] 1518: perf_event name func tag f54b2f831fe8b42d gpl loaded_at 2020-07-20T13:52:58+0900 uid 0 xlated 120B jited 87B memlock 4096B map_ids 1404 btf_id 17
以下のようにして、ロードしたプログラムをダンプすることができます。BCCがBTF付きでプログラムをロードしたため、ソースの情報も確認できます。
% sudo bpftool prog dump xlated id 1518 int func(volatile struct pt_regs * ctx): ; int func(volatile struct pt_regs *ctx) { 0: (18) r1 = 0x200000001 ; struct data_t data = { .a = 1, .b = 2 }; 2: (7b) *(u64 *)(r10 -8) = r1 3: (b7) r1 = 0 ; int key = 0; 4: (63) *(u32 *)(r10 -12) = r1 ; bpf_map_update_elem((void *)bpf_pseudo_fd(1, -1), &key, &data, BPF_ANY); 5: (18) r1 = map[id:1404] 7: (bf) r2 = r10 ; 8: (07) r2 += -12 9: (bf) r3 = r10 10: (07) r3 += -8 ; bpf_map_update_elem((void *)bpf_pseudo_fd(1, -1), &key, &data, BPF_ANY); 11: (b7) r4 = 0 12: (85) call htab_map_update_elem#123424 ; return 0; 13: (b7) r0 = 0 14: (95) exit
また、以下のようにBPFマップもダンプすることができます。
% sudo bpftool map [...] 1404: hash name hash flags 0x0 key 4B value 8B max_entries 10240 memlock 921600B btf_id 17 % sudo bpftool map dump id 1404 [{ "key": 0, "value": { "a": 1, "b": 2 } } ]
ここで、ダンプした際にそのフィールド名(「a」および「b」)とその値が表示されるのはBTFの型情報のおかげです。もしもBTFがなかったら、ただの8バイト(フィールド「a」と「b」の合計サイズ)の値がダンプされることになります。
カーネルのデータ構造を取得するためのBTF
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- 2017年版Linuxカーネル開発レポート公開――支援している企業トップ10とは?
The Linux Foundationは2017年版Linuxカーネル開発レポートを公開した。Linuxカーネル4.8から4.13までの開発に焦点を当て、カーネル開発に携わった開発者や変更数などについて言及した。 - Linuxカーネルのソースコードを読んで、システムコールを探る
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。前回まで、printf()内の中身をさまざまな方法で探り、write()やint $0x80の呼び出しまでたどり着いた。今回は、さらにその先にあるLinuxカーネル側のシステムコールを見ていく。 - SystemTapで真犯人を捕まえろ!