在 WebAssembly 中使用 Rust 编写 eBPF 程序并发布 OCI 镜像

本文介绍了如何使用 Rust 编写 eBPF 程序,然后编译为 WebAssembly 模块。通过 Wasm-bpf 工具链,开发者可以在 Wasm 中编写、动态加载和分发 eBPF 程序,享受 eBPF 的高性能和 Wasm 的沙箱、跨平台优势。文章还展示了如何使用 OCI 镜像发布和管理 eBPF 程序,提供类似 Docker 的体验。

作者:于桐,郑昱笙

eBPF(extended Berkeley Packet Filter)是一种高性能的内核虚拟机,可以运行在内核空间中,以收集系统和网络信息。随着计算机技术的不断发展,eBPF 的功能日益强大,并且已经成为各种效率高效的在线诊断和跟踪系统,以及构建安全的网络、服务网格的重要组成部分。

WebAssembly(Wasm)最初是以浏览器安全沙盒为目的开发的,发展到目前为止,WebAssembly 已经成为一个用于云原生软件组件的高性能、跨平台和多语言软件沙箱环境,Wasm 轻量级容器也非常适合作为下一代无服务器平台运行时,或在边缘计算等资源受限的场景高效执行。

现在,借助 Wasm-bpf 编译工具链和运行时,我们可以使用 Wasm 将 eBPF 程序编写为跨平台的模块,使用 C/C++ 和 Rust 编写程序。通过在 WebAssembly 中使用 eBPF 程序,我们不仅让 Wasm 应用获得 eBPF 的高性能、对系统接口的访问能力,还可以让 eBPF 程序享受到 Wasm 的沙箱、灵活性、跨平台性、和动态加载的能力,并且使用 Wasm 的 OCI 镜像来方便、快捷地分发和管理 eBPF 程序。例如,可以类似 docker 一样,从云端一行命令获取 Wasm 轻量级容器镜像,并运行任意 eBPF 程序。通过结合这两种技术,我们将会给 eBPF 和 Wasm 生态来一个全新的开发体验!

使用 Wasm-bpf 工具链在 Wasm 中编写、动态加载、分发运行 eBPF 程序

在前两篇短文中,我们已经介绍了 Wasm-bpf 的设计思路,以及如何使用 C/C++ 在 Wasm 中编写 eBPF 程序:

基于 Wasm,我们可以使用多种语言构建 eBPF 应用,并以统一、轻量级的方式管理和发布。以我们构建的示例应用 bootstrap.wasm 为例,使用 C/C++ 构建的镜像大小最小仅为 ~90K,很容易通过网络分发,并可以在不到 100ms 的时间内在另一台机器上动态部署、加载和运行,并且保留轻量级容器的隔离特性。运行时不需要内核特定版本头文件、LLVM、clang 等依赖,也不需要做任何消耗资源的重量级的编译工作。对于 Rust 而言,编译产物会稍大一点,大约在 2M 左右。

本文将以 Rust 语言为例,讨论:

  • 使用 Rust 编写 eBPF 程序并编译为 Wasm 模块
  • 使用 OCI 镜像发布、部署、管理 eBPF 程序,获得类似 Docker 的体验

我们在仓库中提供了几个示例程序,分别对应于可观测、网络、安全等多种场景。

编写 eBPF 程序并编译为 Wasm 的大致流程

一般说来,在非 Wasm 沙箱的用户态空间,使用 libbpf-bootstrap 脚手架,可以快速、轻松地使用 C/C++构建 BPF 应用程序。编译、构建和运行 eBPF 程序(无论是采用什么语言),通常包含以下几个步骤:

  • 编写内核态 eBPF 程序的代码,一般使用 C/C++ 或 Rust 语言
  • 使用 clang 编译器或者相关工具链编译 eBPF 程序(要实现跨内核版本移植的话,需要包含 BTF 信息)。
  • 在用户态的开发程序中,编写对应的加载、控制、挂载、数据处理逻辑;
  • 在实际运行的阶段,从用户态将 eBPF 程序加载进入内核,并实际执行。

使用 Rust 编写 eBPF 程序并编译为 Wasm

Rust 可能是 WebAssembly 生态系统中支持最好的语言。Rust 不仅支持几个 WebAssembly 编译目标,而且 wasmtime、Spin、Wagi 和其他许多 WebAssembly 工具都是用 Rust 编写的。因此,我们也提供了 Rust 的开发示例:

  • Wasm 和 WASI 的 Rust 生态系统非常棒
  • 许多 Wasm 工具都是用 Rust 编写的,这意味着有大量的代码可以复用。
  • Spin 通常在对其他语言的支持之前就有Rust的功能支持
  • Wasmtime 是用 Rust编写的,通常在其他运行时之前就有最先进的功能。
  • 可以在 WebAssembly 中使用许多现成的 Rust 库。
  • 由于 Cargo 的灵活构建系统,一些 Crates 甚至有特殊的功能标志来启用Wasm的功能(例如Chrono)。
  • 由于 Rust 的内存管理技术,与同类语言相比,Rust 的二进制大小很小。

我们同样提供了一个 Rust 的 eBPF SDK,可以使用 Rust 编写 eBPF 的用户态程序并编译为 Wasm。借助 aya-rs 提供的相关工具链支持,内核态的 eBPF 程序也可以用 Rust 进行编写,不过在这里,我们还是复用之前使用 C 语言编写的内核态程序。

首先,我们需要使用 rust 提供的 wasi 工具链,创建一个新的项目:

rustup target add wasm32-wasi
cargo new rust-helloworld

之后,可以使用 Makefile 运行 make 完成整个编译流程,并生成 bootstrap.bpf.o eBPF 字节码文件。

使用 wit-bindgen 生成类型信息,用于内核态和 Wasm 模块之间通信

wit-bindgen 项目是一套着眼于 WebAssembly,并使用组件模型的语言的绑定生成器。绑定是用 *.wit 文件描述的,文件中描述了 Wasm 模块导入、导出的函数和接口。我们可以 wit-bindgen 它来生成多种语言的类型定义,以便在内核态的 eBPF 和用户态的 Wasm 模块之间传递数据。

我们首先需要在 Cargo.toml 配置文件中加入 wasm-bpf-bindingwit-bindgen-guest-rust 依赖:

wasm-bpf-binding = { path = "wasm-bpf-binding" }

这个包提供了 wasm-bpf 由运行时提供给 Wasm 模块,用于加载和控制 eBPF 程序的函数的绑定。

  • wasm-bpf-binding 在 wasm-bpf 仓库中有提供。
[dependencies]
wit-bindgen-guest-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", version = "0.3.0" }

[patch.crates-io]
wit-component = {git = "https://github.com/bytecodealliance/wasm-tools", version = "0.5.0", rev = "9640d187a73a516c42b532cf2a10ba5403df5946"}
wit-parser = {git = "https://github.com/bytecodealliance/wasm-tools", version = "0.5.0", rev = "9640d187a73a516c42b532cf2a10ba5403df5946"}

这个包支持用 wit 文件为 rust 客户程序生成绑定。使用这个包的情况下,我们不需要再手动运行 wit-bindgen。

接下来,我们使用 btf2wit 工具,从 BTF 信息生成 wit 文件。可以使用 cargo install btf2wit 安装我们提供的 btf2wit 工具,并编译生成 wit 信息:

cd btf
clang -target bpf -g event-def.c -c -o event.def.o
btf2wit event.def.o -o event-def.wit
cp *.wit ../wit/
这是一个非常前沿且实用的问题:**如何用 Rust 编写 eBPF 程序与 Go 用户态程序协同工作?** > ✅ **答案是:可以!而且这是现代 eBPF 开发的推荐方式之一。** --- ## 🎯 目标架构 我们希望实现: ``` +---------------------+ | Go 用户态程序 | ← 启动、加载、读取数据、上报(服务化) +----------+----------+ ↓ 加载 eBPF 对象文件 (.o) ↓ [Kernel Space] +---------------------+ | Rust 编写eBPF 程序 | ← 监控系统调用、网络、定时器等 +---------------------+ ``` - ✅ **Rust**:编写安全、高性能、可验证的 eBPF 内核程序 - ✅ **Go**:作为运维友好的用户态服务,集成 Prometheus、HTTP API、日志等 - ✅ 协同方式:通过 `.o` ELF 对象文件通信 --- ## 🔧 技术栈说明 | 组件 | 工具 | |------|------| | eBPF 编译器 | `clang`, `llc`, `rustc` | | Rust eBPF 框架 | [`aya-rs/aya`](https://github.com/aya-rs/aya) | | Go eBPF 库 | [`cilium/ebpf`](https://github.com/cilium/ebpf) | | 构建工具 | `cargo`, `make`, `bindgen` | > 💡 `aya` 是目前最成熟的 Rust eBPF 框架,支持 CO-RE、BPF Maps、Perf/Ring Buffers。 --- ## ✅ 实现步骤详解 ### 步骤 1:使用 Rust + Aya 创建 eBPF 程序 #### 初始化项目 ```bash cargo new ebpf-rust --lib cd ebpf-rust ``` #### 修改 `Cargo.toml` ```toml [package] name = "ebpf-rust" version = "0.1.0" edition = "2021" [dependencies] aya = "0.14" aya-log = "0.2" log = "0.4" [lib] crate-type = ["cdylib"] # 必须为 cdylib 才能生成 .o 文件 ``` #### 编写 eBPF 程序 —— `src/lib.rs` ```rust use aya::{ programs::{TracePoint, tracepoint::TracePointContext}, maps::HashMap, include_bytes_aligned, Bpf, }; use log::info; use aya_log::info as aya_info; #[derive(Debug, Clone, Copy)] #[repr(C)] struct Stats { count: u64, } #[no_mangle] static mut STATS: HashMap<u32, Stats> = HashMap::with_max_entries(1024); #[no_mangle] pub extern "C" fn trace_write(ctx: TracePointContext) -> i32 { unsafe { let pid = bpf_get_current_pid_tgid() >> 32; let stats = STATS.get(&pid).unwrap_or(Stats { count: 0 }); stats.count += 1; STATS.insert(&pid, &stats, 0); } 0 } #[cfg(feature = "user")] fn main() { // 这部分不会被编译进 BPF 程序 } aya_bpf::load!(include_bytes_aligned!( "../../target/bpfel-unknown-none/release/ebpf_rust.bpf.o" )); ``` > ⚠️ 注意:`#[no_mangle]` 和 `extern "C"` 是必须的,以便 Go 程序能识别符号。 --- ### 步骤 2:构建 eBPF 程序(生成 `.o` 文件) #### 安装 `cargo-binutils` ```bash rustup target add bpfel-unknown-none cargo install cargo-binutils ``` #### 构建命令 ```bash # 编译为 BPF 字节码 cargo build --release --target bpfel-unknown-none # 输出文件: # target/bpfel-unknown-none/release/ebpf_rust.bpf.o ``` 这个 `.o` 文件就是我们要交给 Go 程序加载的对象文件。 --- ### 步骤 3:Go 用户态程序加载 `.o` 读取数据 #### 项目结构 ```bash go-eBPF-user/ ├── main.go └── ebpf_rust.bpf.o # 从 Rust 构建复制过来 ``` #### `main.go` —— 使用 `cilium/ebpf` 加载监控 ```go // main.go package main import ( "log" "time" "github.com/cilium/ebpf" ) type Stats struct { Count uint64 `json:"count"` } func main() { // 加载由 Rust 编译出的 eBPF 对象 collection, err := ebpf.LoadCollection("ebpf_rust.bpf.o") if err != nil { log.Fatal("Failed to load BPF object: ", err) } defer collection.Close() // 获取 map statsMap := collection.Maps["STATS"] if statsMap == nil { log.Fatal("Map 'STATS' not found in BPF object") } // 定期打印统计信息 ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for range ticker.C { var key uint32 var value Stats iter := statsMap.Iterate() for iter.Next(&key, &value) { log.Printf("PID %d wrote %d times", key, value.Count) } } } ``` --- ### 步骤 4:运行全流程 #### 1. 构建 Rust eBPF 程序 ```bash cd ebpf-rust cargo build --release --target bpfel-unknown-none cp target/bpfel-unknown-none/release/ebpf_rust.bpf.o ../go-eBPF-user/ ``` #### 2. 运行 Go 用户态程序(需要 root) ```bash cd go-eBPF-user sudo go run main.go ``` #### 输出示例: ```text 2025/04/05 15:20:01 PID 1234 wrote 5 times 2025/04/05 15:20:06 PID 1234 wrote 8 times ``` --- ## 🔄 数据流总结 | 阶段 | 操作 | |------|------| | 1. 编写 | Rust 写逻辑,Go 写控制 | | 2. 构建 | `cargo build --target bpfel-unknown-none` → `.bpf.o` | | 3. 传递 | 将 `.bpf.o` 文件交给 Go 程序 | | 4. 加载 | Go 使用 `ebpf.LoadCollection()` 加载它 | | 5. 通信 | 通过 BPF Maps 共享数据(如 `HashMap<PID, Count>`) | --- ## ✅ 优势对比 | 方式 | 优点 | 缺点 | |------|------|------| | **C + Go** | 成熟、文档多 | C 易出错,缺乏类型安全 | | **Rust + Go** | 内存安全、零成本抽象、编译期检查强 | 学习曲线陡峭,工具链较新 | | **Python + BCC** | 快速原型 | 性能差,不适合生产 | | **Go only** | ❌ 不可能直接写 eBPF | - | > ✅ 推荐:**RusteBPF,Go 写用户态服务** —— 结合两者优势! --- ## 🛠️ 最佳实践建议 1. ✅ 使用 `aya` 框架编写 Rust eBPF 程序 2. ✅ 使用 `cilium/ebpf` 在 Go 中加载 `.o` 文件 3. ✅ 将 `.bpf.o` 嵌入 Go 二进制(使用 `go:embed`) 4. ✅ 使用 `CO-RE` 提高跨内核兼容性 5. ✅ 日志使用 `aya-log` 而非 `println!`(避免 panic) --- ## 📦 可选优化:将 `.o` 文件嵌入 Go 二进制 ```go //go:embed ebpf_rust.bpf.o var bpfObject []byte func loadBpfObject() (*ebpf.Collection, error) { return ebpf.LoadCollectionWithOptions(bytes.NewReader(bpfObject), ...) } ``` 这样就不依赖外部文件了。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值