翼度科技»论坛 云主机 LINUX 查看内容

how-to-write-rust-in-wasm

5

主题

5

帖子

15

积分

新手上路

Rank: 1

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

作者:于桐,郑昱笙
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 工具链,创建一个新的项目:
  1. rustup target add wasm32-wasi
  2. 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-binding 和 wit-bindgen-guest-rust 依赖:
  1. wasm-bpf-binding = { path = "wasm-bpf-binding" }
复制代码
这个包提供了 wasm-bpf 由运行时提供给 Wasm 模块,用于加载和控制 eBPF 程序的函数的绑定。

  • wasm-bpf-binding 在 wasm-bpf 仓库中有提供。
  1. [dependencies]
  2. wit-bindgen-guest-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", version = "0.3.0" }
  3. [patch.crates-io]
  4. wit-component = {git = "https://github.com/bytecodealliance/wasm-tools", version = "0.5.0", rev = "9640d187a73a516c42b532cf2a10ba5403df5946"}
  5. 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 信息:
  1. cd btf
  2. clang -target bpf -g event-def.c -c -o event.def.o
  3. btf2wit event.def.o -o event-def.wit
  4. cp *.wit ../wit/
复制代码

  • 其中 event-def.c 是包含了我们需要的结构体信息的的 C 程序文件。只有在导出符号中用到的结构体才会被记录在 BTF 中。
对于 C 结构体生成的 wit 信息,大致如下:
  1. default world host {
  2.     record event {
  3.          pid: s32,
  4.         ppid: s32,
  5.         exit-code: u32,
  6.         --pad0: list<s8>,
  7.         duration-ns: u64,
  8.         comm: list<s8>,
  9.         filename: list<s8>,
  10.         exit-event: s8,
  11.     }
  12. }
复制代码
wit-bindgen-guest-rust 会为 wit 文件夹中的所有类型信息,自动生成 rust 的类型,例如:
  1. #[repr(C, packed)]
  2. #[derive(Debug, Copy, Clone)]
  3. struct Event {
  4.     pid: i32,
  5.     ppid: i32,
  6.     exit_code: u32,
  7.     __pad0: [u8; 4],
  8.     duration_ns: u64,
  9.     comm: [u8; 16],
  10.     filename: [u8; 127],
  11.     exit_event: u8,
  12. }
复制代码
编写用户态加载和处理代码

为了在 WASI 上运行,需要为 main.rs 添加 #![no_main] 属性,并且 main 函数需要采用类似如下的形态:
  1. #[export_name = "__main_argc_argv"]
  2. fn main(_env_json: u32, _str_len: i32) -> i32 {
  3.     return 0;
  4. }
复制代码
用户态加载和挂载代码,和 C/C++ 中类似:
  1.     let obj_ptr =
  2.         binding::wasm_load_bpf_object(bpf_object.as_ptr() as u32, bpf_object.len() as i32);
  3.     if obj_ptr == 0 {
  4.         println!("Failed to load bpf object");
  5.         return 1;
  6.     }
  7.     let attach_result = binding::wasm_attach_bpf_program(
  8.         obj_ptr,
  9.         "handle_exec\0".as_ptr() as u32,
  10.         "\0".as_ptr() as u32,
  11.     );
  12.     ...
复制代码
polling ring buffer:
  1.     let map_fd = binding::wasm_bpf_map_fd_by_name(obj_ptr, "rb\0".as_ptr() as u32);
  2.     if map_fd < 0 {
  3.         println!("Failed to get map fd: {}", map_fd);
  4.         return 1;
  5.     }
  6.     // binding::wasm
  7.     let buffer = [0u8; 256];
  8.     loop {
  9.         // polling the buffer
  10.         binding::wasm_bpf_buffer_poll(
  11.             obj_ptr,
  12.             map_fd,
  13.             handle_event as i32,
  14.             0,
  15.             buffer.as_ptr() as u32,
  16.             buffer.len() as i32,
  17.             100,
  18.         );
  19.     }
复制代码
使用 handler 接收返回值:
[code]extern "C" fn handle_event(_ctx: u32, data: u32, _data_sz: u32) {    let event_slice = unsafe { slice::from_raw_parts(data as *const Event, 1) };    let event = &event_slice[0];    let pid = event.pid;    let ppid = event.ppid;    let exit_code = event.exit_code;    if event.exit_event == 1 {        print!(            "{:

举报 回复 使用道具