4 个版本 (2 个破坏性更新)
使用旧的 Rust 2015
0.2.0 | 2023 年 5 月 31 日 |
---|---|
0.1.0 | 2018 年 10 月 25 日 |
0.0.2 | 2017 年 1 月 8 日 |
0.0.1 | 2017 年 1 月 8 日 |
#345 在 Unix API
2,152 每月下载量
用于 6 个 crate (4 个直接)
290KB
4K SLoC
包含 (ELF 可执行文件/库, 1KB) examples/load_elf__block_a_port.o
rbpf

Rust (用户空间) eBPF 虚拟机
描述
此 crate 包含一个用于 eBPF 程序执行的虚拟机。BPF(类似于伯克利包过滤器)是一种汇编语言,最初是为 BSD 系统开发的,目的是通过像 tcpdump 这样的工具在内核中过滤数据包,以避免将不必要的数据复制到用户空间。它已被移植到 Linux,在那里它演变成 eBPF(扩展 BPF),一个功能更强大、速度更快的版本。虽然 BPF 程序最初是打算在内核中运行的,但此 crate 的虚拟机允许在用户空间应用程序中运行它;它包含一个解释器、一个 eBPF 程序的 x86_64 JIT 编译器以及一个反汇编器。
它基于 Rich Lane 的 uBPF 软件,几乎做了同样的事情,但它是用 C 编写的。
该 crate 应该可以在 Linux、MacOS X 和 Windows 上编译和运行,尽管 JIT 编译器目前不支持 Windows。
链接到 crate
该 crate 可从 crates.io 获取,因此您只需将其添加到您的 Cargo.toml
文件中的依赖项即可正常工作
[dependencies]
rbpf = "0.2.0"
您也可以从该 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
API的源代码中已经有了很好的文档。您还应该能够从这里访问在线文档的版本,它自动从crates.io版本生成(可能不如主分支最新)。示例和单元测试也应该很有帮助。以下是使用此包的总结。
以下是运行rbpf中的eBPF程序的步骤
- 创建一个虚拟机。有几种不同的机器类型,我们稍后将会详细介绍。创建VM时,将eBPF程序作为参数传递给构造函数。
- 如果您想使用一些辅助函数,请将它们注册到虚拟机中。
- 如果您需要一个即时编译(JIT)的程序,请编译它。
- 执行您的程序:运行解释器或调用JIT编译的函数。
eBPF最初是为了过滤数据包而设计的(现在它在Linux内核中也有一些其他钩子,如kprobes,但rbpf不涵盖这一点)。因此,程序的大部分加载和存储指令都是在表示数据包数据的内存区域上执行。然而,在Linux内核中,eBPF程序并不立即访问这个数据区域:最初,它访问一个C结构体struct sk_buff
,这是一个包含数据包元数据的缓冲区,包括数据包数据区域的开始和结束的内存地址。因此,程序首先从sk_buff
中加载这些指针,然后可以访问数据包数据。
这种行为可以用rbpf复制,但这不是强制性的。因此,我们有几个结构体表示不同类型的虚拟机
-
struct EbpfVmMbuffer
模仿内核。当程序运行时,提供给其第一个eBPF寄存器的地址将是用户提供的元数据缓冲区的地址,它预计将包含指向数据包数据内存区域开始和结束的指针。 -
struct EbpfVmFixedMbuff
有一个目的:启用与内核兼容的程序执行,同时节省用户手动处理元数据缓冲区的努力。实际上,这个结构体有一个静态内部缓冲区,该缓冲区被传递给程序。用户必须指示eBPF程序期望在缓冲区中找到数据包数据开始和结束的偏移值。在调用运行程序的功能(JIT编译或非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时将None
传递给new()
方法),则不会运行验证器。
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内核兼容的程序,这可能是有用的,因为这些程序必须使用特定的辅助编号。
// 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() {
// 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);
}
使用JIT在数据包上
此示例来自单元测试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
];
// This is an eBPF VM for programs reading from a given memory area (it
// directly reads from packet data)
let mut vm = rbpf::EbpfVmRaw::new(Some(prog)).unwrap();
#[cfg(windows)] {
assert_eq!(vm.execute_program(mem).unwrap(), 0x11);
}
#[cfg(not(windows))] {
// This time we JIT-compile the program.
vm.jit_compile().unwrap();
// Then we execute it. For this kind of VM, a reference to the packet
// data must be passed to the function that executes the program.
unsafe { assert_eq!(vm.execute_program_jit(mem).unwrap(), 0x11); }
}
}
使用元数据缓冲区
此示例来自单元测试test_jit_mbuff
,并源自单元测试test_jit_ldxh
。
extern crate rbpf;
fn main() {
let prog = &[
// Load mem from mbuff at offset 8 into R1
0x79, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
// ldhx r1[2], r0
0x69, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
];
let mem = &mut [
0xaa, 0xbb, 0x11, 0x22, 0xcc, 0xdd
];
// Just for the example we create our metadata buffer from scratch, and
// we store the pointers to packet data start and end in it.
let mut mbuff = &mut [0u8; 32];
unsafe {
let mut data = mbuff.as_ptr().offset(8) as *mut u64;
let mut data_end = mbuff.as_ptr().offset(24) as *mut u64;
*data = mem.as_ptr() as u64;
*data_end = mem.as_ptr() as u64 + mem.len() as u64;
}
// This eBPF VM is for program that use a metadata buffer.
let mut vm = rbpf::EbpfVmMbuff::new(Some(prog)).unwrap();
#[cfg(windows)] {
assert_eq!(vm.execute_program(mem, mbuff).unwrap(), 0x2211);
}
#[cfg(not(windows))] {
// Here again we JIT-compile the program.
vm.jit_compile().unwrap();
// Here we must provide both a reference to the packet data, and to the
// metadata buffer we use.
unsafe {
assert_eq!(vm.execute_program_jit(mem, mbuff).unwrap(), 0x2211);
}
}
}
从对象文件加载代码;并使用虚拟元数据缓冲区
这来自单元测试 test_vm_block_port
。
此示例需要以下额外的crate,您可能需要将它们添加到您的 Cargo.toml
文件中。
[dependencies]
rbpf = "0.2.0"
elf = "0.0.10"
它还使用一种VM,该VM使用内部缓冲区来模拟内核中eBPF程序使用的sk_buff
,无需为每个数据包手动创建新的缓冲区。这可能对编译为内核且假定它们接收到的数据是sk_buff
的程序有用,该sk_buff
指向数据包数据的起始和结束地址。因此,我们只需提供eBPF程序期望找到这些指针的偏移量,VM处理缓冲区更新,这样我们只需为程序运行的每次运行提供数据包数据的引用。
extern crate elf;
use std::path::PathBuf;
extern crate rbpf;
use rbpf::helpers;
fn main() {
// Load a program from an ELF file, e.g. compiled from C to eBPF with
// clang/LLVM. Some minor modification to the bytecode may be required.
let filename = "examples/load_elf__block_a_port.o";
let path = PathBuf::from(filename);
let file = match elf::File::open_path(&path) {
Ok(f) => f,
Err(e) => panic!("Error: {:?}", e),
};
// Here we assume the eBPF program is in the ELF section called
// ".classifier".
let text_scn = match file.get_section(".classifier") {
Some(s) => s,
None => panic!("Failed to look up .classifier section"),
};
let prog = &text_scn.data;
// This is our data: a real packet, starting with Ethernet header
let packet = &mut [
0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54,
0x08, 0x00, // ethertype
0x45, 0x00, 0x00, 0x3b, // start ip_hdr
0xa6, 0xab, 0x40, 0x00,
0x40, 0x06, 0x96, 0x0f,
0x7f, 0x00, 0x00, 0x01,
0x7f, 0x00, 0x00, 0x01,
0x99, 0x99, 0xc6, 0xcc, // start tcp_hdr
0xd1, 0xe5, 0xc4, 0x9d,
0xd4, 0x30, 0xb5, 0xd2,
0x80, 0x18, 0x01, 0x56,
0xfe, 0x2f, 0x00, 0x00,
0x01, 0x01, 0x08, 0x0a, // start data
0x00, 0x23, 0x75, 0x89,
0x00, 0x23, 0x63, 0x2d,
0x71, 0x64, 0x66, 0x73,
0x64, 0x66, 0x0a
];
// This is an eBPF VM for programs using a virtual metadata buffer, similar
// to the sk_buff that eBPF programs use with tc and in Linux kernel.
// We must provide the offsets at which the pointers to packet data start
// and end must be stored: these are the offsets at which the program will
// load the packet data from the metadata buffer.
let mut vm = rbpf::EbpfVmFixedMbuff::new(Some(prog), 0x40, 0x50).unwrap();
// We register a helper function, that can be called by the program, into
// the VM.
vm.register_helper(helpers::BPF_TRACE_PRINTK_IDX,
helpers::bpf_trace_printf).unwrap();
// This kind of VM takes a reference to the packet data, but does not need
// any reference to the metadata buffer: a fixed buffer is handled
// internally by the VM.
let res = vm.execute_program(packet).unwrap();
println!("Program returned: {:?} ({:#x})", res, res);
}
构建 eBPF 程序
除了传递构建eBPF程序的原始十六进制代码外,还有两种方法可用。
汇编器
第一种方法是在crate中使用的汇编器。
extern crate rbpf;
use rbpf::assembler::assemble;
let prog = assemble("add64 r1, 0x605
mov64 r2, 0x32
mov64 r1, r0
be16 r0
neg64 r2
exit").unwrap();
println!("{:?}", prog);
上述片段将产生
Ok([0x07, 0x01, 0x00, 0x00, 0x05, 0x06, 0x00, 0x00,
0xb7, 0x02, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00,
0xbf, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xdc, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0x87, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
相反,还有一个反汇编器,可以将字节码中的指令名称以人类友好的格式输出。
extern crate rbpf;
use rbpf::disassembler::disassemble;
let prog = &[
0x07, 0x01, 0x00, 0x00, 0x05, 0x06, 0x00, 0x00,
0xb7, 0x02, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00,
0xbf, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xdc, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0x87, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
];
disassemble(prog);
这将产生以下输出
add64 r1, 0x605
mov64 r2, 0x32
mov64 r1, r0
be16 r0
neg64 r2
exit
构建API
构建程序的另一种方法是使用指令构建器API中的命令链。它看起来不太像汇编,可能更像高级函数。可以肯定的是,结果是更冗长的,但如果您更喜欢以这种方式构建程序,它同样有效。如果我们再次使用上面的相同示例,它将如下构建。
extern crate rbpf;
use rbpf::insn_builder::*;
let mut program = BpfCode::new();
program.add(Source::Imm, Arch::X64).set_dst(1).set_imm(0x605).push()
.mov(Source::Imm, Arch::X64).set_dst(2).set_imm(0x32).push()
.mov(Source::Reg, Arch::X64).set_src(0).set_dst(1).push()
.swap_bytes(Endian::Big).set_dst(0).set_imm(0x10).push()
.negate(Arch::X64).set_dst(2).push()
.exit().push();
请再次参阅源代码和相关测试以获取更多信息和使用示例。
欢迎反馈!
这是作者第一次尝试编写Rust代码。在这个过程中,作者学到了很多,但仍然有一种感觉,这个crate在某些地方有一种C风格,而不是作者希望拥有的Rust风格。因此,欢迎反馈(或PR),包括您可能看到如何更好地利用Rust特性的方法。
请注意,项目期望新的提交应受开发者原创证书的保护。在提交拉取请求时,请相应地签署您的提交。
问题 / 答案
为什么要在Rust中实现eBPF虚拟机?
截至本文撰写时,根据作者的最好知识,这个crate没有特定的用途。作者偶然与Linux上的BPF一起工作,并了解uBPF的工作原理,他想学习和实验Rust——没有更多了。
与uBPF有什么区别?
除了显然的语言之外?好吧,还有一些区别
-
一些常量,例如程序的最大长度或堆栈长度,在uBPF和rbpf之间不同。后者使用与Linux内核相同的值,而uBPF有自己的值。
-
当程序在uBPF中运行时发生错误,运行程序的函数静默返回最大值作为错误代码,而rbpf返回Rust类型
Error
。 -
辅助函数的注册,可以在eBPF程序内调用,不是以相同的方式处理的。
-
允许在数据包数据上或与元数据缓冲区(模拟或不模拟)上运行程序的独立结构是rbpf的一个特性。
-
关于性能:理论上JIT编译的程序应该以相同的速度运行,而uBPF的C解释器应该比rbpf稍快。但这还没有得到确认。对这两个程序进行基准测试将是一件有趣的事情。
我可以与“经典”的BPF(即cBPF)版本一起使用吗?
不行。这个crate只与扩展BPF(eBPF)程序一起工作。例如,对于tcpdump(截至本文撰写时)使用的cBPF程序,你可能对亚历山大·波拉科夫编写的bpfjit crate感兴趣。
实现了哪些功能?
运行和JIT编译eBPF程序是可行的。还有一个机制来注册用户定义的帮助函数。Linux内核的eBPF实现提供了一些额外的功能:大量的帮助函数,几种类型的映射,尾调用。
-
添加额外的帮助函数应该很简单,但到目前为止,很少复制现有的Linux帮助函数。
-
尾调用(从eBPF程序到另一个程序的“长跳”)尚未实现。这可能不容易设计和实现。
-
与映射的交互是通过特定的帮助函数完成的,因此添加它应该不难。映射本身可以重用内核中的映射(如果是在Linux上),例如与内核中的eBPF程序进行通信;或者它们可以在用户空间中处理。Rust有数组和哈希表,因此它们的实现应该相当简单(可能在未来添加到rbpf中)。
那么程序验证怎么办?
这个crate的“验证器”非常短,与内核验证器无关,这意味着它接受可能不安全的程序。另一方面,你在这里可能不会运行在内核中,所以它不会使你的系统崩溃。实现一个类似于内核中的验证器并不简单,我们也不能“复制”它,因为它是在GPL许可证下。
那么安全性如何?
Rust非常注重安全性。然而,为了使eBPF VM工作,需要使用一些unsafe
代码块。VM,作为一个eBPF解释器,可以返回错误,但不应崩溃。如果有问题,请提交问题。
至于JIT编译器,情况不同,因为在汇编中实现运行时内存检查更复杂。如果你的JIT编译程序尝试执行未经授权的内存访问,它将会崩溃。通常,首先用解释器测试你的程序是个好主意。
哦,如果你的程序有无限循环,即使是解释器,那也只好自求多福了。
注意事项
-
这个crate正在开发中,API可能会发生变化。
-
JIT编译器生成的是不安全的程序:运行时没有测试内存访问(尚不)。请谨慎使用。
-
一些eBPF指令尚未实现。这对大多数eBPF程序来说应该不是问题。
-
小心芜菁。芜菁很恶心。
待办事项 列表
- 实现一些特质(如
Clone
、Drop
、Debug
)。 - 提供用户空间数组和哈希BPF映射的内置支持。
- 通过运行时内存检查提高JIT编译程序的安全性。
- 添加帮助函数(一些在内核中支持的帮助函数,如校验和更新,可能很有用)。
- 改进验证器。我们能否找到一种直接支持用clang编译的程序的方法?
- 也许有一天,尾调用?
- 其他架构的JIT编译器?
- …
许可
随着Rust语言项目本身的努力,为了简化与其他项目的集成,rbpf crate在MIT许可和Apache许可(版本2.0)的条款下分发。
有关详细信息,请参阅LICENSE-APACHE和LICENSE-MIT。
灵感来源
-
uBPF,一个eBPF虚拟机的C用户空间实现,包括JIT编译器和反汇编器(还包括从指令的可读形式(如
mov r0, 0x1337
)到汇编器的转换),由Rich Lane为Big Switch Networks(2015年)开发 -
在Rust中构建简单的JIT,由Jonathan Turner(2015年)撰写
-
bpfjit(也在crates.io上),一个Rust包,将FreeBSD 10树中的cBPF JIT编译器导出为Rust,由Alexander Polakov(2016年)开发
其他资源
-
Cilium项目关于BPF的文档:BPF和XDP参考指南
-
内核关于BPF的文档:Documentation/networking/filter.txt文件
-
深入BPF:阅读材料列表,一篇博客文章,列出了BPF和相关技术的文档(2016年)
依赖项
约3MB
约68K SLoC