
eBPFプログラム用のRust仮想マシンおよびJITコンパイラ
eBPF 用の Rust(ユーザー空間)仮想マシン
このクレートには、eBPF プログラム実行用の仮想マシンが含まれています。BPF とは Berkeley Packet Filter のことで、もともと BSD システム向けに開発された アセンブリに似た言語であり、tcpdump などのツールを使ってカーネル内でパケットを フィルタリングし、ユーザー空間への無駄なコピーを避けるためのものです。その後 Linux に移植され、より高速で多機能な eBPF(extended BPF)へと進化しました。 BPF プログラムは本来カーネル内で実行されることを想定していますが、このクレートの 仮想マシンを使えばユーザー空間のアプリケーションで実行できます。インタプリタ、 eBPF プログラム用の x86_64 JIT コンパイラ、そして逆アセンブラが含まれています。
これは Rich Lane による uBPF ソフトウェア を ベースにしており、ほぼ同じことを行いますが、C で書かれています。
このクレートは Linux、MacOS X、Windows でコンパイルおよび実行できるはずですが、 現時点では JIT コンパイラは Windows では動作しません。
このクレートは crates.io から入手できるので、
Cargo.toml ファイルに依存関係として追加するだけでそのまま動作するはずです:
[dependencies]
rbpf = "0.4.1"
開発版はこの GitHub リポジトリからも利用できます。これは
Cargo.toml に以下を記述するだけで簡単に導入できます:
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }
もちろん、必要であればローカルにクローンし、場合によってはクレートを改変して、Cargo.toml でローカルバージョンのパスを指定することもできます:
[dependencies]
rbpf = { path = "path/to/rbpf" }
次に、ソースコード内でそのクレートを使用することを指定します:
extern crate rbpf;
APIはソースコード内でかなりよく文書化されています。ここからオンライン版のドキュメントにもアクセスできるはずで、これはcrates.ioのバージョンから自動生成されたものです(メインブランチと同期していない可能性があります)。Examplesとunit testsも役立つはずです。以下はこのクレートの使用方法の概要です。
rbpfでeBPFプログラムを実行するために従うべき手順は次のとおりです:
eBPFはもともとパケットをフィルタリングするために設計されました(現在ではLinuxカーネル内にkprobesなど他のフックもありますが、これはrbpfではカバーされていません)。その結果、プログラムのロード命令とストア命令のほとんどは、パケットデータを表すメモリ領域に対して実行されます。しかし、Linuxカーネルでは、eBPFプログラムはこのデータ領域に直接アクセスしません:最初は、代わりにCのstruct sk_buffにアクセスでき、これはパケットに関するメタデータ(パケットデータ領域の先頭と末尾のメモリアドレスを含む)を含むバッファです。したがって、プログラムはまずsk_buffからこれらのポインタをロードし、その後パケットデータにアクセスできます。
この動作はrbpfで再現できますが、必須ではありません。このため、異なる種類の仮想マシンを表すいくつかの構造体があります:
struct EbpfVmMbufferはカーネルを模倣します。プログラムが実行されると、最初のeBPFレジスタに提供されるアドレスは、ユーザーが提供するメタデータバッファのアドレスとなり、そのバッファにはパケットデータメモリ領域の先頭と末尾へのポインタが含まれていることが期待されます。
struct EbpfVmFixedMbuffには1つの目的があります:カーネルと互換性を持つように作成されたプログラムの実行を可能にしつつ、ユーザーがメタデータバッファを手動で処理する手間を省くことです。実際、この構造体にはプログラムに渡される静的な内部バッファがあります。ユーザーは、eBPFプログラムがバッファ内のパケットデータの先頭と末尾を見つけると期待するオフセット値を指定する必要があります。プログラムを実行する関数(JITされたかどうかに関わらず)を呼び出すと、この構造体は、プログラムが呼び出されたパケットデータの先頭と末尾について、指定されたオフセットでこの静的バッファ内のアドレスを自動的に更新します。
struct EbpfVmRawは、パケットデータ上で直接実行したいプログラム用です。メタデータバッファは関与せず、eBPFプログラムは最初のレジスタでパケットデータのアドレスを直接受け取ります。これはuBPFの動作です。
struct EbpfVmNoDataはデータを一切取りません。eBPFプログラムは引数を一切取らず、その戻り値は決定的です。これに有効なユースケースがあるかはよくわかりませんが、少なくとも単体テストには非常に便利です。
これらの構造体はすべて同じパブリック関数を実装しています:
// called with EbpfVmMbuff:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmMbuff<'a>, Error>
// called with EbpfVmFixedMbuff:: prefix
pub fn new(prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> Result<EbpfVmFixedMbuff<'a>, Error>
// called with EbpfVmRaw:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmRaw<'a>, Error>
// called with EbpfVmNoData:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmNoData<'a>, Error>
これはVMの新しいインスタンスを作成するために使用されます。戻り値の型は、関数が呼び出される構造体に依存します。例えば、rbpf::EbpfVmRaw::new(Some(my_program)) は struct rbpf::EbpfVmRaw のインスタンス(Result でラップされている)を返します。プログラムがロードされると、非常に単純な検証器(Linuxカーネルのものとは全く異なる)でチェックされます。ユーザーはこれをカスタム検証器に置き換えることもできます。
struct EbpfVmFixedMbuff の場合、コンストラクタに2つの追加引数を渡す必要があります:data_offset と data_end_offset です。これらは、プログラムが実行されるたびに、パケットデータのメモリ領域の先頭と末尾へのポインタが内部メタデータバッファに格納される際のオフセット(バイト数)です。他の構造体はこのメカニズムを使用しないため、これらのオフセットは必要ありません。
// for struct EbpfVmMbuff, struct EbpfVmRaw and struct EbpfVmRawData
pub fn set_program(&mut self, prog: &'a [u8]) -> Result<(), Error>
// for struct EbpfVmFixedMbuff
pub fn set_program(&mut self, prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> Result<(), Error>
例えば my_vm.set_program(my_program); を使用して、VM インスタンス作成後にロードされたプログラムを変更できます。このプログラムは VM に接続された verifier でチェックされます。VM の検証関数はいつでも変更できます。
pub type Verifier = fn(prog: &[u8]) -> Result<(), Error>;
pub fn set_verifier(&mut self,
verifier: Verifier) -> Result<(), Error>
なお、プログラムがすでにVMにロードされている場合、新しいverifierを設定すると、ロードされたプログラムに対して即座に実行されます。ただし、プログラムがロードされていない場合(VM作成時にnew()メソッドにNoneが渡された場合)は、verifierは実行されません。
pub type Helper = fn (u64, u64, u64, u64, u64) -> u64;
pub fn register_helper(&mut self,
key: u32,
function: Helper) -> Result<(), Error>
この関数はヘルパー関数を登録するために使用されます。VM はレジスタをハッシュマップに格納するため、キーには任意の u32 値を使用できます。Linux カーネルとの互換性が必要で、特定のヘルパー番号を使用しなければならないプログラムにとって有用かもしれません。
pub fn register_allowed_memory(&mut self, addrs_range: Range<u64>) -> ()
この関数は、eBPFプログラムがロードおよびストアを許可されるメモリアドレスのリストを追加します。この関数を複数回呼び出すと、アドレスが内部のHashSetに追加されます。現時点では、rbpfはインタプリタを使用する場合にのみメモリアクセスを検証します。この関数は、eBPFマップに格納されたオブジェクトへのポインタを返すカーネルヘルパーを使用する場合に役立ちます。
// for struct EbpfVmMbuff
pub fn execute_program(&self,
mem: &'a mut [u8],
mbuff: &'a mut [u8]) -> Result<(u64), Error>
// for struct EbpfVmFixedMbuff and struct EbpfVmRaw
pub fn execute_program(&self,
mem: &'a mut [u8]) -> Result<(u64), Error>
// for struct EbpfVmNoData
pub fn execute_program(&self) -> Result<(u64), Error>
ロードされたプログラムを解釈実行する。この関数は、使用するVMの種類に応じて、パケットデータとメタデータバッファへの参照、パケットデータのみへの参照、または何も受け取らない。返される値はeBPFプログラムの結果である。
pub fn jit_compile(&mut self) -> Result<(), Error>
ロードされたプログラムをx86_64アーキテクチャ向けにJITコンパイルします。プログラムがヘルパー関数を使用する場合、この関数が呼び出される前にそれらをVMに登録する必要があります。生成されたアセンブリ関数はVM内部に保存されます。
// for struct EbpfVmMbuff
pub unsafe fn execute_program_jit(&self, mem: &'a mut [u8],
mbuff: &'a mut [u8]) -> Result<(u64), Error>
// for struct EbpfVmFixedMbuff and struct EbpfVmRaw
pub unsafe fn execute_program_jit(&self, mem: &'a mut [u8]) -> Result<(u64), Error>
// for struct EbpfVmNoData
pub unsafe fn execute_program_jit(&self) -> Result<(u64), Error>
JIT コンパイルされたプログラムを呼び出します。指定する引数は execute_program() の場合と同じで、これも使用する VM の種類によって異なります。JIT コンパイルされたプログラムの結果はインタプリタの場合と同じになるはずですが、より高速に実行されるはずです。プログラム実行中にエラーが発生した場合、JIT コンパイル版はインタプリタほど適切に処理できず、プログラムがクラッシュする可能性があることに注意してください。このため、これらの関数は unsafe とマークされています。
これはユニットテスト test_vm_add から引用したものです。
extern crate rbpf;
fn main() {