2 个版本

0.1.1 2021 年 3 月 11 日
0.1.0 2021 年 3 月 9 日

#423 in Unix API

MIT 许可证

49KB
559 行代码(不含注释)


sonde

crates.io documentation

sonde 是一个将 USDT 探针编译成 Rust 库的库,并为其生成友好的 Rust 风格 API。

用户定义静态跟踪探针(简称 USDT)是一种从 DTrace(参见 OpenDtrace 了解更多信息)继承的技术。它允许用户在他们的应用程序中定义静态跟踪探针,而传统上这些探针是在内核中声明的。

USDT 探针可以自然地与 DTrace 消费,也可以与 eBPF(例如 bccbpftrace 等)一起使用。

轻量级探针设计

库和可执行文件中的 USDT 探针定义在相应应用程序二进制文件中的 ELF 部分。探针被转换为一个 nop 指令,其元数据存储在 ELF 的 .note.stapstd 部分。当注册探针时,USDT 工具(如 dtracebccbpftrace 等)将读取 ELF 部分,并将指令从 nop 修改为 breakpoint,然后运行附加的跟踪事件。注销探针后,USDT 将从 breakpoint 恢复 nop 指令。

当没有工具监听探针时,使用 USDT 探针的开销几乎为零,否则可以观察到微小的开销。

工作流程

一切都是自动化的。不过,在编译时系统上必须有 dtrace。让我们想象以下 sonde-test 这个虚构项目

/sonde-test
├── src
│  ├── main.rs
├── build.rs
├── Cargo.toml
├── provider.d

从最明显的事情开始:让我们在 Cargo.toml 文件中添加以下行

[build-dependencies]
sonde = "0.1"

现在,让我们看看 provider.d 文件里有什么。它不是针对 sonde 的特定供应商格式,而是声明 USDT 探针的规范方法(见 脚本)!

provider hello {
    probe world(); 
    probe you(char*, int);
};

它描述了一个名为 hello 的探针提供者,包含两个探针。

  1. world,
  2. you 探针有 2 个参数:char*int

请注意,D 类型的类型与 C 类型的类型不完全相同,即使它们看起来一样。

在这个步骤中,需要使用 dtrace -s 来编译探针到系统陷阱头文件或对象文件,但别担心,sonde 会为你处理。让我们看看 build.rs 脚本里有什么。

fn main() {
    sonde::Builder::new()
        .file("./provider.d")
        .compile();
}

就是这样。这就是使它正常工作的最小代码。

最终,我们希望在代码中触发这个探针。那么让我们看看 src/main.rs 里面有什么。

// Include the friendly Rust idiomatic API automatically generated by
// `sonde`, inside a dedicated module, e.g. `tracing`.
mod tracing {
    include!(env!("SONDE_RUST_API_FILE"));
}

fn main() {
    tracing::hello::world();

    println!("Hello, World!");
}

这里有什么?tracing 模块包含一个 hello 模块,对应于 hello 提供者。并且这个模块包含一个 world 函数,对应于 world 探针。太棒了!

看看 SONDE_RUST_API_FILE 指向的文件内容。
/// Bindings from Rust to the C FFI small library that calls the
/// probes.

use std::os::raw::*;

extern "C" {
    #[doc(hidden)]
    fn hello_probe_world();

    #[doc(hidden)]
    fn hello_probe_you(arg0: *mut c_char, arg1: c_int);
}

/// Probes for the `hello` provider.
pub mod r#hello {
    use std::os::raw::*;

    /// Call the `world` probe of the `hello` provider.
    pub fn r#world() {
        unsafe { super::hello_probe_world() };
    }

    /// Call the `you` probe of the `hello` provider.
    pub fn r#you(arg0: *mut c_char, arg1: c_int) {
        unsafe { super::hello_probe_you(arg0, arg1) };
    }
}

让我们看看它的实际效果。

$ cargo build --release
$ sudo dtrace -l -c ./target/release/sonde-test | rg sonde-test
123456 hello98765 sonde-test hello_probe_world world

太酷了!我们的 sonde-test 可执行文件包含了来自 hello 提供者的 world 探针!

$ # Let's execute `sonde-test` as usual.
$ ./target/release/sonde-test
Hello, World!
$
$ # Now, let's execute it with `dtrace` (or any other tracing tool).
$ # Let's listen the `world` probe and prints `gotcha!` when it's executed.
$ sudo dtrace -n 'hello*:::world { printf("gotcha!\n"); }' -q -c ./target/release/sonde-test
Hello, World!
gotcha!

嘿,它工作了!现在让我们尝试使用 you 探针。

fn main() {
    {
        let who = std::ffi::CString::new("Gordon").unwrap();
        tracing::hello::you(who.as_ptr() as *mut _, who.as_bytes().len() as _);
    }

    println!("Hello, World!");
}

是时候展示一下了。

$ cargo build --release
$ sudo dtrace -n 'hello*:::you { printf("who=`%s`\n", stringof(copyin(arg0, arg1))); }' -q -c ./target/release/sonde-test
Hello, World!
who=`Gordon`

在 USDT 探针内部成功读取一个字符串!

使用 sonde,您可以通过简单地编辑您的规范 .d 文件来添加尽可能多的探针到您的 Rust 库或二进制文件中。

额外功能:sonde 会自动为您的探针生成文档。运行 cargo doc --open 来检查。

可能的限制

类型

DTrace 有自己的类型系统(接近 C)(见 数据类型和大小)。sonde 尽可能地将它映射到 Rust 系统中,但某些类型可能无法匹配。以下类型被支持

D 语言中的类型名称 Rust 中的类型名称
char std::os::raw::c_char
short std::os::raw::c_short
int std::os::raw::c_int
long std::os::raw::c_long
long long std::os::raw::c_longlong
int8_t i8
int16_t i16
int32_t i32
int64_t i64
intptr_t isize
uint8_t u8
uint16_t u16
uint32_t u32
uint64_t u64
uintptr_t usize
float std::os::raw::c_float
double std::os::raw::c_double
T* *mutT
T** *mut *mut T(等等)

解析器

..d 文件由 sonde 解析。目前,只解析了 provider 块,这些块声明了 probe。目前忽略了所有的预处理器指令(#pragma)。

许可证

BSD-3-Clause,见 LICENSE.md

依赖项

~4–12MB
~155K SLoC