連載
» 2020年08月06日 05時00分 公開

Berkeley Packet Filter(BPF)入門(10):単なるデバッグ情報だけではない「BPF Type Format」(BTF)の使い道

Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、最近のBPFの発展に欠かすことのできない重要機能「BPF Type Format(BTF)」について。

[味曽野雅史,OSSセキュリティ技術の会]

 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.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。