本文地址:https://www.ebpf.top/post/01-intro-into-ebpf-and-rust

在本文中,我们将探讨如何使用 eBPF 和 Rust 构建高性能的 Linux 追踪工具。无论你是刚刚开始接触 eBPF,还是对如何将 eBPF 与现代 Rust 工具集成感到好奇,你都可以在本文有所收获。本文将带你了解 eBPF 基础知识、Rust 开发工具选择以及实际的系统调用追踪示例。

1. 什么是 eBPF?

eBPF 是一种允许开发者在 Linux 内核中运行安全的沙盒程序技术,而无需修改内核源代码或加载内核模块。通常这些运行程序会用于网络、可观测性和安全性。

eBPF 程序在内核中运行于一个受限的虚拟机中(类似于 WebAssembly (WASM) 或 JVM 在用户空间中的运行方式),这种运行机制确保 eBPF 程序运行时候的行为,从而保障安全性。

eBPF 技术的最大优势之一是它可以动态扩展内核行为,而无需等待上游补丁或重启系统。由于其运行在内核上下文中,因此可以最小的开销来进行系统观察和事件响应,这非常适合高性能的追踪、过滤和执行任务的场景。可以在 ebpf.io 找到更深入的解释。

2. 为什么选择 Rust?

Rust 是一种强大的现代系统编程语言,本身没有垃圾回收机制,专注于性能、内存安全和并发性。Rust 非常适合与 eBPF 技术组合使用,原因有以下两点:

  • 内存安全:与 C 不同,Rust 语言可以防止常见的错误,如空指针解引用、缓冲区溢出和使用后释放,这些预发机制与低级内核接口交互时至关重要。
  • 强大的工具链:像 Cargo、rust-analyzer、clippy 和 libbpf-cargo 等工具使得在 Rust 中构建和测试 eBPF 应用程序变得无缝衔接。

3. 可用的 Rust 库

3.1 libbpf-rs

  • 安全且符合 Rust 习惯的包装器,底层基于广泛使用的 C 语言 libbpf 库。

  • 其开发设计和维护者与 C 语言 libbpf 相同。

  • bpftoollibbpf-cargo 集成良好。

  • 仍然需要用 C 编写 eBPF 程序,但可用 Rust 编写用户空间加载器。

  • 最佳场景:生产级系统、高兼容性和需要 CO-RE(Compile Once Run Everywhere)机制。

链接:libbpf-rs GitHub

3.2 Aya

  • 纯 Rust 的 eBPF 工具链,可基于 Rust 编写用户空间和 eBPF 程序。

  • 不依赖 C 或 clang 工具链。

  • 支持 XDP、tracepoint、uprobes、perf buffer、map 等。

  • 仍在快速成熟中,但有强大的社区支持。

  • 最佳场景:全 Rust 工具链、嵌入式场景友好,且具有更好的移植性。

链接:Aya 官网

4. 为什么选择 libbpf-rs

在深入探讨 libbpf-rs 之前,我们需要先理解 libbpf 本身的作用和重要性。

4.1 什么是 libbpf?

当你编写和编译 eBPF 程序时,通常会依赖内核数据类型的内部结构。然而,这些内核数据结构可能在不同 Linux 版本之间发生变化——例如,Linux 内核版本 5.6 中的某个结构体可能在 Linux 6.1 版本中增加了一个字段或调整了布局。不同内核版本数据结构调整可能导致编写的 eBPF 程序行为异常,甚至无法通过新内核的验证。

为了解决这个版本兼容性问题,libbpf 库(通常被称为 eBPF 加载器)引入了 CO-RE(Compile Once, Run Everywhere)能力。CO-RE 允许编译一次 eBPF 程序,然后在不同内核版本上运行,而无需重新编译。

4.2 CO-RE 的工作原理

CO-RE 底层依赖 vmlinux.h,这是一个包含当前运行内核所有类型定义的头文件,该头文件可通过工具 bpftool 生成:

1
$  bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

vmlinux.h 文件依据内核的 BTF(BPF Type Format)数据生成,我们可在 .bpf.c 程序中安全且可移植地访问内核结构。

当 eBPF 程序加载时,libbpf 库会检查程序访问的字段,并确保它们与实际运行内核中的数据布局对齐—即使相关的结构体在当前运行的内核中已经发生了变化。CO-RE 机制通过加载时的重定位逻辑实现这一点,使得程序能适配不同内核版本而无需硬编码特定布局。如需技术细节,推荐阅读 Andrii Nakryiko 关于 CO-RE 的博客文章

4.3 为什么选择 libbpf-rs

我最初尝试了使用纯 Rust 工具链实现的 aya-rs 库,其允许我们使用 Rust 语言编写用户态程序和内核中 eBPF 程序。

Aya 设计非常现代,但在使用某些 eBPF 辅助函数时,我遇到了校验器拒绝价值的错误。经过调查和社区反馈(GitHub Issue),我发现这些问题源于 Rust 编译器(rustc)与 CO-RE 的兼容性问题。由于 Aya 针对 CO-RE 支持尚不成熟,且依赖实验性的 Rust LLVM 特性,因此决定转向 libbpf-rs,这是因为使用 libbpf-rs 不仅可基于稳定 libbpf C 库,还可使用符合 Rust 语言习惯的良好封装。

4.5 为什么 libbpf-rs 适合我的场景

libbpf-rs 提供一流的 CO-RE 机制支持,意味着可移植性是内置的。其次使用 libbpf-rs 库,我可用 C 语言编写 eBPF 程序,这与大多数生产级开源项目(如 Cilium、Tracee 等)保持一致。

同时,围绕 eBPF 的生态系统和文档大多是使用或者假设使用 C 语言编写的。使用 C 语言编写 eBPF 程序更容易基于现有实例扩展,更容易让人理解和沟通交流。

而用户空间逻辑使用 Rust,可让我在依赖稳定的内核交互基础的同时,享受 Rust 生态系统的安全性和工具生态。

5. eBPF + Rust:架构图

以下是 eBPF 程序与 Rust 用户空间应用集成的架构概览:

  • 编写 eBPF 程序及配套的 Rust 用户空间代码;

  • 编译源代码生成 eBPF 字节码和 Rust 二进制文件;

  • 运行用户空间二进制程序;

  • 用户空间程序尝试将 eBPF 字节码加载至内核;

  • eBPF 验证器检查程序是否满足内核安全要求;

  • 若验证失败则拒绝加载,验证通过则可通过 JIT 编译将字节码转为机器指令,并加载到内核;

  • 待 eBPF 程序加载完成后,用户空间程序将 eBPF 程序挂载到相关内核挂载点上;

  • Rust 用户空间与内核空间的通信通过 eBPF Maps 实现;

我们将在后续文章中通过实践案例深入探讨这些核心机制。

6. 使用样例:用 eBPF 和 Rust 追踪系统调用

介绍了基础知识后,让我们通过追踪 Linux 系统上系统调用示例来进行展示。项目结构如下所以,在运行之前,请确保你的系统满足 eBPF 的前提条件,完整代码参见 GitHub 仓库

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
└── syscall-tracer
    ├── build.rs   # Builds the C code and generates eBPF bytecode & Rust bindings
    ├── Cargo.lock
    ├── Cargo.toml
    ├── Cross.toml # Cross-compilation configuration
    ├── README.md
    └── src
        ├── bpf
        │   ├── syscall.bpf.c  # eBPF program
        │   ├── syscall.skel.rs # Auto-generated bindings by libbpf-cargo
        │   └── vmlinux.h # CO-RE header for kernel type definitions
        ├── lib.rs
        ├── log.rs
        └── main.rs   # Rust userspace: loads, attaches eBPF and handles kernel events

以下是跟踪系统调用时输出输出的样例:

7. 结论

无论你是在构建可观测性工具,还是在实验内核内部机制,eBPF + Rust 都提供了一个强大且安全的基础。而我们才刚刚开始。

原文地址:https://dev.to/maheshrayas/-intro-into-ebpf-and-rust-l2m

作者:Mahesh Rayas