RustだけでeBPFを動作させよう!

FUJITA Tomonori
nttlabs
Published in
Jun 24, 2021

前回の記事で言及した、eBPFを利用したソフトウェアをRustで実装するためのライブラリは、C言語のlibbpfライブラリを呼び出しています。純粋なRustへの愛を貫くため、libbpfを捨て、RustだけでeBPFを動かしたい。

libbpfとは

libbpfは、デファクトのeBPFのライブラリで、C言語で実装されています。

eBPFを活用するソフトウェアは、カーネルスペースで動作するeBPFバイトコードと、eBPFバイトコードを制御するユーザスペースのアプリケーションから構成されます。libbpfは、eBPFのバイトコードを、静的なデータとしてアプリケーションのバイナリに埋め込みます。ELF形式のアプリケーションのバイナリファイルの中に、ELF形式のeBPFバイトコードが埋め込まれていることになります。

アプリケーションのバイナリにBPFのバイトコードが埋め込まれている

アプリケーションは起動すると、libbpfの関数を呼び出し、静的データとして保存されているeBPFのバイトコードの解析、bpfシステムコールによる関数やマップなど必要なデータのカーネルへロード、イベントへのアタッチなどを実行します。

なお、libbpfとは違うやり方でeBPFを扱う、一昔前主流だったeBPFのライブラリBCCが話題になった場合は、「BCCはプロダクション環境で使えるデザインじゃないですね」などのように、相手の発言をさえぎりましょう。相手はあなたの知識の深さに驚き、あなたが実際にプロダクション環境に関わっているのかとか、具体的なデザインについては、質問してこないはずです。

最も困難な課題

eBPFのライブラリを実装する上で、最も困難な課題は、Linuxカーネルの構造体がカーネルのバージョンやカーネルコンフィグレーションで、変わってしまうということです。

listenシステムコールを呼び出したプロセスのプロセス番号を表示するeBPFのコードを考えてみます。4行目で、プロセスに関する情報を保持しているtask_struct構造体が保存されたメモリにアクセスし、6行目でpidメンバの値を得ています。

task_struct構造体にアクセスするeBPFコード

次に、コンパイラが生成したバイトコードを見てみます。1番目の命令で、task_struct構造体のpidメンバのバイト・オフセット位置が2,328バイトにセットされています。このバイト・オフセット位置は、コンパイル時のホストで動作していたカーネルの構造体の情報です。

コンパイラが生成したeBPFバイトコード

このeBPFバイトコードが埋め込まれたアプリケーションは、カーネルバージョンやカーネルコンフィグレーションが異なる、つまり、task_struct構造体のpidメンバのバイト・オフセット位置が異なるカーネルが動作するホストでは、動かないということになります。

バイナリ書き換え

アプリケーションのバイナリが、特定のカーネルでしか動かないポータビリティの問題を解決するため、libbpfチームが開発したのが、CO-RE (Compile Once — Run Everywhere)技術です。埋め込まれたeBPFのバイトコードがカーネルにロードされる前に、ホストで動作しているカーネルの構造体に一致するように書き換えられます。

他のホストでアプリケーションを立ち上げ、カーネルにロードされたeBPFバイトコードを確認してみると、1番目の命令で使われているバイト・オフセット位置が2,328から2,336に書き換えられていることがわかります。このホストで動作するカーネルのtask_struct構造体のpidメンバのバイト・オフセット位置が2,336ということです。

他のホストで起動した際にカーネルにロードされたeBPFのバイトコード

eBPFバイトコードを書き換えるために必要な情報を管理する仕組みがBTFです。バイトコードのELFには、コンパイラが生成したBTF情報が含まれています。バイトコードのELFのセクションを確認すると、.BTFにはコンパイル時のカーネルの構造体情報、.BTF.extにはバイトコードの書き換えに関する情報が保存されています。今回の例では、libbpfは、それらの情報から、1番目の命令がtask_struct構造体のpidメンバのバイト・オフセット位置を含んでいて、書き変えの必要があるかもしれないことを知ります。libbpfは、アプリケーションが起動されたホストで動作するカーネルの構造体情報を、/sys/kernel/btf/vmlinuxファイルから得て、task_struct構造体のpidメンバのバイト・オフセット位置に合わせて、バイナリが書き換えます。

eBFPバイトコードのELFセクション情報

まとめ

今回の例では、単純なバイト・オフセット位置の違いへの対応でしたが、変数名の変更など、CO-REは様々な複雑な構造体の違いに対応できるように設計されています。

eBPFの純粋なRustライブラリの実装は、バイトコードのELFフォーマットの解釈、カーネルへのロード、アタッチなどの機能はRust愛で乗り越えられると思いますが、CO-REに関しては、「起動時にしか使われない、これほど複雑な機能(C言語で実装ずみ)をRustで再実装する意義とは…?」と、愛に疑いが生じるかもしれません。愛を超え、心を無にして、何も疑わず、全てを受け入れる、Rustへの信仰が答えなのかもしれません。

数日前、Rustのみで実装されたeBPFライブラリAyaが公開されました。まだ、libbpfの機能が網羅されているようには見えませんが、Rust愛ですね。負けてはいられないというエンジニアのみなさま、NTTは仲間を募集中です。

--

--

FUJITA Tomonori
nttlabs

Janitor at the 34th floor of NTT Tamachi office, had worked on Linux kernel, founded GoBGP, TGT, Ryu, RustyBGP, etc. https://twitter.com/brewaddict