🐝
eBPF
#tech
2023-11-19
Linuxカーネルには,eBPFという機能が存在する.これは仮想マシンの様な顔をしていながらシステムオブザービリティなどにも有用である大変に面白いものなので,紹介する.
そもそもeBPFの"e"はextendedを意味する.そのため当然extendedでないBPFが存在するわけだがこれは人によっては単純に"BPF"と呼んだり,classicなBPFなので"cBPF"と呼んだりするわけだが,本記事では区別が分かりやすいと思うので"cBPF"で呼称することにする.最初に,BPFの概観について見てからeBPFについて見ることにする.
BPFは,"Berkeley Packet Filter"のアクロニムであり,1992年にBerkeley研究所の論文で概念が発表された.その後1997年にcBPFがLinuxカーネルに組み込まれて2014年にはeBPFが搭載されるようになった.
基本的には,カーネルランドで動作する汎用仮想マシンであるが,名前の通り当初はパケットフィルタリングを行うことを意図して設計されたものとなっている.カーネルイベントをフックして,カーネルランドの情報収集が可能となっている.
そもそものcBPFが生まれた経緯として,OSが行う通信のパケットの一部を抽出したいとなった場合,素直にやるのであれば,ユーザーランドのアプリケーションがカーネルランドがキャプチャしたパケットたちを受け取り,その後にユーザーランド側でフィルタリングを行う.このような構成は言うなればユーザーレベルでのパケットフィルタリングをしている.
しかしよく考えると,このような構成よりも,カーネルランド側でフィルタリングをする方が,カーネルランドとユーザーランドの切り替え回数は減るため,オーバーヘッドが減り,パフォーマンスの向上が見込めそうである.これがcBPFの提案においての動機であり,元論文にも書かれている.
Linuxには,KVM(Kernel-based Virtual Machine)という,/dev/kvm
にioctl
を介して通信することでユーザーランドから操作が行える仮想マシンを提供する機能が存在する.こちらはハードウェアまで含めてエミュレートするものとなっているが,BPFは独自命令セットを有する仮想CPUのようなものであるという違いがある.eBPFのアーキテクチャは,後述する.
さらに言えばBPFの機能はカーネルモジュールがあれば事足りそうに思える.しかしカーネルモジュールは言うなれば自由度が高いものであり,時として危険なプログラムが走る.これに対してeBPFではプログラムの実行前に安全かどうかの検査が入るため,より安全にカーネルの機能を用いることができるといえる.
eBPFは,システムコールを介してアクセスすることができる.この定義はLinuxカーネルのinclude/uapi/linux/bpf.h
内に存在する.ドキュメントの形式で見たいならこれとかを読むとよさそう.主要なものを挙げてみると,
- BPF_MAP_CREATE: eBPFマップ(後述)を作成してそのfdを返す
- BPF_PROG_ATTACH: fdを指定してeBPFのプログラムをアタッチする
- BPF_PROG_LOAD: eBPFのプログラムを検査してカーネルランドにロードする
などがある.
ざっとこんな感じだと思う:
まずユーザーランドでは,eBPFの機能を用いるプログラムを作成する必要がある.このプログラムはコンパイルによってeBPFのバイトコードに変換され,その後システムコールを介してカーネルランドにこれがロードされる.
カーネルランドにバイトコードが持ち込まれると,kernel/bpf/verifier.c
において定義されているeBPFのVerifierが動作し,持ち込まれたバイトコードが正常か(危険性が存在しなさそうか)などを検査する.このときに検査される項目は,例えば次のようなものがある:
- ループが存在しない
- invalidなメモリアクセスを行わない
- アラインメントが不正でない
- unreachableな命令が無い
- 命令数が多くない[1]
実際には,Verifierがチェックする項目はeBPFのプログラムタイプに依存する.これらは次のようなものがあり,ユーザーランドのアプリケーションを作成する際にロードするeBPFのプログラムによって決まる:
Verifierのチェックをパスすると,arch/mips/net/bpf_jit_comp.c
にあるJITコンパイラがバイトコードを機械語にコンパイルする.もしターゲットが未対応だったりJITが無効[2]な環境であった場合は,コンパイルせずにインタプリタを用いて実行される.
コンパイルされた機械語は,kernel/bpf/core.c
にあるExecuterによって,実際にユーザーランドのプログラムがeBPFを用いる際にロードされる.
ユーザーランドに存在する,eBPFの機能をその中で用いるようなプログラムは,各種システムコールを介してeBPFのイベントを発火させる.イベントが発火すると,前述のバイナリが呼ばれ,結果をeBPFマップやeBPFリングバッファに格納する.これらはユーザーランドからもread
システムコールを介して参照可能であるため,ユーザーランドのアプリケーションはここで処理結果を受け取れる.
eBPFの命令セットは,次のような構造体で表されている:
cBPFの場合は,処理される際にカーネル内でバイトコードがeBPF用に変換される.このbpf_insn
構造体のメンバはそれぞれ
code
: オペコードdst_reg
: 宛て先となるレジスタsrc_reg
: ソースとなるレジスタoff
: オフセットimm
: 即値
を表しており,各命令は64bitの固定長となっている.
レジスタは,次のようなものが存在する:
- R0: 返り値を格納する
- R1~R5: 引数レジスタ
- R6~R9: 汎用レジスタ
- R10: read onlyなフレームポインタ
スタックは512バイト分だけある.BPFでは16バイトなのを考えると大分増えている.このスタックにはR10を介してアクセスできる.
cBPFが考案された目的がオーバーヘッドを減らしたパケットフィルタリングであったので,パケットキャプチャやイベントのトレーシングに用いることができる.
Linuxカーネルに存在するseccomp
という,プロセスが用いるシステムコールの実行を制限する機構などにもeBPFは用いられている.