Linux 5.5におけるBPF(Berkeley Packet Filter)の新機能Berkeley Packet Filter(BPF)入門(7)

Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、Linux 5.5で導入されたBPFに関する主な新機能について。

» 2020年02月18日 05時00分 公開
[味曽野雅史OSSセキュリティ技術の会]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

 Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載「Berkeley Packet Filter(BPF)入門」。

 2020年1月26日にLinux 5.5がリリースされたので、今回は通常の連載内容を中断して、Linux 5.5で導入されたBPFに関する主な新機能を紹介します。

BPF Trampoline

 BPF TrampolineはカーネルからBPFコードをほぼオーバーヘッドなしで呼び出すための仕組みです。動的にJIT(Just-In-Time)コンパイルしたBPFプログラムへのジャンプコードを生成し、それをBPFプログラム呼び出しに利用します。

利点

 従来であればBPFプログラムを呼び出すときには、引数の準備やBPFプログラム関数のルックアップ処理などのオーバーヘッドがありますが、BPF Trampolineによって、その部分を最小限に抑えることができます。

 またBPF Trampolineを利用すると、BPFプログラム呼び出し時に関数ポインタを利用する必要がなくなります。結果としてSpectre対策の「Retpoline」のオーバーヘッドもなくなります。現時点ではまだ対応していませんが、将来的には特にパフォーマンスクリティカルなXDP(eXpress Data Path)におけるBPFプログラム呼び出しの高速化が期待されています。

カーネル関数の呼び出し/戻りをトレースする機能

 このBPF Trampolineを利用して、カーネル関数の呼び出し/戻りをトレースする機能が追加されました。BPFプログラムは「BPF_PROG_TYPE_TRACING」というプログラムタイプになります。このとき、従来ある「ftrace」の機能を応用し、BPF Trampolineで生成したBPF呼び出しのコードを実行します。

 ftraceはLinuxのトレース機構です。ftraceにはさまざまな機能がありますが、主要となる機能の一つが関数の呼び出し/戻りのトレースです。関数の呼び出しをトレースするには単純には関数の先頭でトレース用の関数を呼び出せばよいでしょう。しかし、単純に実装してしまうとトレースしていないときでもオーバーヘッドが発生してしまいます。そこでftraceは関数先頭に「nop」を埋め込みます(カーネルコンパイル時に処理します)。トレースを有効にした際にその箇所をパッチしてトレース処理を行います。BPF_PROG_TYPE_TRACINGでは、このnopの部分を生成したBPF Trampolineへのcall命令に書き換えてBPFプログラムを呼び出します。

BPF Trampolineの具体例

 BPF Trampolineの具体例として、ソースコードのコメントから「__be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev);」関数の例を引用します。

 まず、ftraceを有効にしてコンパイルしたvmlinuxでこの「eth_type_trans」関数をディスアセンブルすると、以下のようになっています。

% gdb vmlinux --ex 'disassemble eth_type_trans'
[...]
   0xffffffff830465f0 <+0>:     callq  0xffffffff83601960 <__fentry__>
   0xffffffff830465f5 <+5>:     push   %rbp
   0xffffffff830465f6 <+6>:     mov    %rsp,%rbp
   0xffffffff830465f9 <+9>:     push   %r15
   0xffffffff830465fb <+11>:    push   %r14
[...]

 ここで、関数先頭にある「__fentry__」がftraceのエントリポイントです。この部分は、通常時はnopに置き換えられています。「BPF_PROG_TYPE_TRACING」で関数呼び出しをトレースする場合、この「__fentry__」の部分が作成したBPF Trampolineコードへの呼び出し命令になります。BPF Trampolineのコードの実体は以下のようになります。

push rbp
mov rbp, rsp
sub rsp, 16                     // space for skb and dev
push rbx                        // temp regs to pass start time
mov qword ptr [rbp - 16], rdi   // save skb pointer to stack
mov qword ptr [rbp - 8], rsi    // save dev pointer to stack
call __bpf_prog_enter           // rcu_read_lock and preempt_disable
mov rbx, rax                    // remember start time in bpf stats are enabled
lea rdi, [rbp - 16]             // R1==ctx of bpf prog
call addr_of_jited_FENTRY_prog
movabsq rdi, 64bit_addr_of_struct_bpf_prog  // unused if bpf stats are off
mov rsi, rbx                    // prog start time
call __bpf_prog_exit            // rcu_read_unlock, preempt_enable and stats math
mov rdi, qword ptr [rbp - 16]   // restore skb pointer from stack
mov rsi, qword ptr [rbp - 8]    // restore dev pointer from stack
pop rbx
leave
ret

 上の「addr_of_jited_FENTRY_prog」がBPFプログラムの呼び出し処理です。必要最小限のコードでBPFプログラムが呼び出されていることが確認できます。

BPFプログラムの引数の構成

 さて、BPF Trampolineは単純にBPFプログラムにジャンプするだけではありません。もう一つ重要な処理としてBPFプログラムの引数の構成があります。BPF_PROG_TYPE_TRACINGのBPFプログラムの場合、BPFプログラムからトレース対象となる関数の引数にアクセスできます。これを実現するために、BPF Trampolineのコードはトレース対象となる関数の引数をスタックに積み、そのスタックのアドレスをBPFプログラムの引数として渡します。BPFプログラム側からはこのアドレスを利用して適切に値をロードすることで、目的の引数にアクセスします。

 それでは、BPF Trampolineはどうやって関数の引数の箇所を求めるのでしょうか? 関数の引数の構成方法はABI(Application Binary Interface)により定められています。例えば「SytemV AMD64 ABI」の場合、RDI(デスティネーションインデックス)レジスタが第1引数、RSI(ソースインデックス)レジスタが第2引数、……というように決まっています。従って、それに応じてスタックに値を積めばよいことになります。しかし、ここでさらにもう一つ問題があります。それは、トレース対象となる関数の引数の数をどうやって知るかという問題です。ただのバイナリのコードからでは関数の引数の情報は分かりません。

BTF(BPF Type Format)

 この問題の救世主がBTF(BPF Type Format)です。BTFは特にBPFによる利用を想定したデバッグ情報のフォーマットです。デバッグ情報といえば「DWARF」が有名ですが、DWARFは情報が多い分、サイズが大きくなります。特にLinuxの場合、数MBの「vmlinux」ファイルがDWARFでは数百MBになります。BTFは持つ情報を関数の引数や、構造体の情報などに限定することで、デバッグ情報のサイズを小さくし、vmlinuxでもデバッグ情報を数MB程度に抑えます。

 Linuxは4.18からBTFに対応し、コンパイル時にBTFのデバッグ情報を生成することができます。BPF_PROG_TYPE_TRACINGのBPFプログラムに関するBPF Trampolineを生成する際は、このBTFの情報を参照し、適切にトレース対象の関数の引数をスタックに積むコードを生成します。

 ユーザーから見るとBPF_PROG_TYPE_TRACINGの動作は、大まかには関数先頭に「k(ret)probe」コマンドでBPFプログラムをアタッチした場合と同じですが。こちらの方が前述の通り、呼び出し時のオーバーヘッドが少なくなります。また、関数の戻り時に関数の引数にアクセスすることができます。これは現在のkretprobeにアタッチしたBPFプログラムからは行えません。現状kretprobeから関数の引数にアクセスしたい場合は、kprobeにアタッチしたBPFプログラムでBPFマップに必要な情報を格納し、それを後から参照する必要があります。また、kprobeは関数の任意のオフセットにアタッチできますが、BPF_PROG_TYPE_TRACINGではそれはできないという違いもあります。

BPFプログラムの型検査

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

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

メールマガジン登録

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