Linux 5.5におけるBPF(Berkeley Packet Filter)の新機能:Berkeley Packet Filter(BPF)入門(7)
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、Linux 5.5で導入されたBPFに関する主な新機能について。
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.
関連記事
- 2018年上半期に話題になったSpectreとその変異、Linuxカーネルでの対応まとめ
連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説していく。2018年の上半期は、「Meltdown」「Spectre」とその変異(Variant)の脆弱性に悩まされた。今回はいつもとは異なり、上半期のまとめも兼ねて、Meltdown/Spectreの各変異をバージョンを追いかけながら整理する。 - 2017年版Linuxカーネル開発レポート公開――支援している企業トップ10とは?
The Linux Foundationは2017年版Linuxカーネル開発レポートを公開した。Linuxカーネル4.8から4.13までの開発に焦点を当て、カーネル開発に携わった開発者や変更数などについて言及した。 - Linuxカーネルのソースコードを読んで、システムコールを探る
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。前回まで、printf()内の中身をさまざまな方法で探り、write()やint $0x80の呼び出しまでたどり着いた。今回は、さらにその先にあるLinuxカーネル側のシステムコールを見ていく。