Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/hywan/sonde-rs

A library to compile USDT probes into a Rust library
https://github.com/hywan/sonde-rs

dtrace ebpf probe rust rust-lang rust-library usdt

Last synced: about 1 month ago
JSON representation

A library to compile USDT probes into a Rust library

Awesome Lists containing this project

README

        




sonde

[![crates.io](https://img.shields.io/crates/v/sonde)](https://crates.io/crates/sonde)
[![documentation](https://img.shields.io/badge/doc-sonde-green)](https://docs.rs/sonde)

`sonde` is a library to compile USDT probes into a Rust library, and
to generate a friendly Rust idiomatic API around it.

[Userland Statically Defined Tracing][usdt] probes (USDT for short) is
a technique inherited from [DTrace] (see [OpenDtrace] to learn
more). It allows user to define statically tracing probes in their own
application; while they are traditionally declared in the kernel.

USDT probes can be naturally consumed with DTrace, but also with
[eBPF] (`bcc`, `bpftrace`…).

## Lightweight probes by design

USDT probes for libraries and executables are defined in an ELF
section in the corresponding application binary. A probe is translated
into a `nop` instruction, and its metadata are stored in the ELF's
`.note.stapstd` section. When registering a probe, USDT tool (like
`dtrace`, `bcc`, `bpftrace` etc.) will read the ELF section, and
instrument the instruction from `nop` to `breakpoint`, and after that,
the attached tracing event is run. After deregistering the probe, USDT
will restore the `nop` instruction from `breakpoint`.

The overhead of using USDT probes is almost zero when no tool is
listening the probes, otherwise a tiny overhead can be noticed.

## The workflow

Everything is automated. `dtrace` must be present on the system at
compile-time though. Let's imagine the following `sonde-test`
fictitious project:

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

Start with the obvious thing: let's add the following lines to the
`Cargo.toml` file:

```toml
[build-dependencies]
sonde = "0.1"
```

Now, let's see what is in the `provider.d` file. It's _not_ a `sonde`
specific vendor format, it's the canonical way to declare USDT probes
(see [Scripting][scripting])!

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

It describes a probe provider, `hello`, with two probes:

1. `world`,
2. `you` with 2 arguments: `char*` and `int`.

Be careful, D types aren't the same as C types, even if they look like
the same.

At this step, one needs to play with `dtrace -s` to compile the probes
into systemtrap headers or an object file, but forget about that,
`sonde` got you covered. Let's see what's in the `build.rs` script:

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

That's all. That's the minimum one needs to write to make it
work.

Ultimately, we want to fire this probe from our code. Let's see what's
inside `src/main.rs` then:

```rust
// 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!");
}
```

What can we see here? The `tracing` module contains a `hello` module,
corresponding to the `hello` provider. And this module contains a
`world` function, corresponding to the `world` probe. Nice!

See what's contained by the file pointed by SONDE_RUST_API_FILE:

```rust
/// 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) };
}
}
```

Let's see it in action:

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

Neat! Our `sonde-test` binary contains a `world` probe from the
`hello` provider!

```sh
$ # 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!
```

Eh, it works! Let's try with the `you` probe now:

```rust
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!");
}
```

Time to show off:

```sh
$ 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`
```

Successfully reading a string from Rust inside a USDT probe!

With `sonde`, you can add as many probes inside your Rust library or
binary as you need by simply editing your canonical `.d` file.

Bonus: `sonde` generates documentation for your probes
automatically. Run `cargo doc --open` to check.

## Possible limitations

### Types

DTrace has its own type system (close to C) (see [Data Types and
Sizes][data-types]). `sonde` tries to map it to the Rust system as
much as possible, but it's possible that some types could not
match. The following types are supported:

| Type Name in D | Type Name in 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*` | `*mut T` |
| `T**` | `*mut *mut T` (and so on) |

### Parser

The `.d` files are parsed by `sonde`. For the moment, only the
`provider` blocks are parsed, which declare the `probe`s. All the
pragma (`#pragma`) directives are ignored for the moment.

## License

`BSD-3-Clause`, see `LICENSE.md`.

[usdt]: https://illumos.org/books/dtrace/chp-usdt.html
[DTrace]: https://en.wikipedia.org/wiki/DTrace
[OpenDtrace]: https://github.com/opendtrace/opendtrace
[eBPF]: http://www.brendangregg.com/blog/2019-01-01/learn-ebpf-tracing.html
[data-types]: https://illumos.org/books/dtrace/chp-typeopexpr.html#chp-typeopexpr-2
[`std::os::raw`]: https://doc.rust-lang.org/std/os/raw/index.html
[scripting]: https://illumos.org/books/dtrace/chp-script.html#chp-script