用于 eBPF 的 Rust(用户空间)虚拟机
此 crate 包含一个用于执行 eBPF 程序的虚拟机。BPF,即 Berkeley Packet Filter(伯克利包过滤器),是一种类似汇编的语言,最初为 BSD 系统开发,目的是使用 tcpdump 等工具在内核中过滤数据包,从而避免向用户空间进行无用的拷贝。它后来被移植到 Linux,并演变为 eBPF(extended BPF),一个具有更多特性的更快版本。虽然 BPF 程序最初设计为在内核中运行,但此 crate 的虚拟机使其能够在用户空间应用程序中运行;它包含一个解释器、一个用于 eBPF 程序的 x86_64 JIT 编译器,以及一个反汇编器。
它基于 Rich Lane 的 uBPF 软件,后者功能几乎相同,但使用 C 语言编写。
此 crate 应当能够在 Linux、MacOS X 和 Windows 上编译和运行,尽管 JIT 编译器目前尚不支持 Windows。
此 crate 可从 crates.io 获取,因此只需将其作为依赖项添加到你的 Cargo.toml 文件中即可开箱即用:
[dependencies]
rbpf = "0.4.1"
你也可以使用此 GitHub 仓库中的开发版本。只需将其放入你的 Cargo.toml 中即可:
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }
当然,如果你愿意,也可以在本地克隆它,或许还可以修改这个 crate,
然后在 Cargo.toml 中指明你本地版本的路径:
[dependencies]
rbpf = { path = "path/to/rbpf" }
然后在你的源代码中表明你想使用该 crate:
extern crate rbpf;
API 在源代码中有相当完善的文档。你还应该能够访问在线版本的文档,它由 crates.io 版本自动生成(可能不是最新的主分支版本)。示例和单元测试也应该会有所帮助。以下是关于如何使用该 crate 的摘要。
以下是使用 rbpf 运行 eBPF 程序需要遵循的步骤:
eBPF 最初设计用于过滤数据包(现在它在 Linux 内核中还有一些其他钩子,例如 kprobes,但 rbpf 不涵盖这些)。因此,程序的大部分加载和存储指令都是在代表数据包数据的内存区域上执行的。然而,在 Linux 内核中,eBPF 程序并不会立即访问这个数据区域:最初,它访问的是一个 C 语言 struct sk_buff,这是一个包含数据包元数据的缓冲区——包括数据包数据区域起始和结束的内存地址。因此,程序首先从 sk_buff 中加载这些指针,然后才能访问数据包数据。
这种行为可以用 rbpf 来复现,但并非强制要求。为此,我们有几种表示不同类型虚拟机的结构体:
struct EbpfVmMbuffer 模拟内核。当程序运行时,提供给其第一个 eBPF 寄存器的地址将是用户提供的元数据缓冲区的地址,该缓冲区预期包含指向数据包数据内存区域起始和结束位置的指针。
struct EbpfVmFixedMbuff 有一个目的:允许执行为与内核兼容而创建的程序,同时为用户省去手动处理元数据缓冲区的麻烦。实际上,这个结构体有一个静态内部缓冲区,会传递给程序。用户必须指明 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,必须向构造函数传递两个额外的参数: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 的验证器进行检查。VM 的验证函数可以随时更改。
pub type Verifier = fn(prog: &[u8]) -> Result<(), Error>;
pub fn set_verifier(&mut self,
verifier: Verifier) -> Result<(), Error>
请注意,如果程序已经加载到 VM 中,设置新的验证器也会立即对已加载的程序运行它。但是,如果没有加载任何程序(如果在创建 VM 时向 new() 方法传递了 None),则验证器不会运行。
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>
解释已加载的程序。该函数根据所使用的虚拟机类型,接收对数据包数据和元数据缓冲区的引用,或仅接收对数据包数据的引用,或完全不接收任何参数。返回的值是 eBPF 程序的结果。
pub fn jit_compile(&mut self) -> Result<(), Error>
对已加载的程序进行 JIT 编译,目标架构为 x86_64。如果程序需要使用辅助函数,则必须在调用此函数之前将它们注册到 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() {
// This is the eBPF program, in the form of bytecode instructions.
let prog = &[
0xb4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov32 r0, 0
0xb4, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, // mov32 r1, 2
0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // add32 r0, 1
0x0c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // add32 r0, r1
0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // exit
];
// Instantiate a struct EbpfVmNoData. This is an eBPF VM for programs that
// takes no packet data in argument.
// The eBPF program is passed to the constructor.
let vm = rbpf::EbpfVmNoData::new(Some(prog)).unwrap();
// Execute (interpret) the program. No argument required for this VM.
assert_eq!(vm.execute_program().unwrap(), 0x3);
}
这来自单元测试 test_jit_ldxh。
extern crate rbpf;
fn main() {
let prog = &[
0x71, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // ldxh r0, [r1+2]
0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // exit
];
// Let's use some data.
let mem = &mut [
0xaa, 0xbb, 0x11, 0xcc, 0xdd
];