Skip to content

Commit

Permalink
Simply use backtrace-rs to look up symbols
Browse files Browse the repository at this point in the history
  • Loading branch information
cloneable committed Apr 3, 2023
1 parent 414f242 commit efb84fc
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 180 deletions.
46 changes: 27 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,41 @@ The microchassis is all about increasing the observability of Rust binaries.
Example intergration with `hyper` endpoint:

```rust
use microchassis::profiling::jeprof;
use microchassis::profiling::{jeprof, mallctl};
```

```rust
let symbol_table = Arc::new(jeprof::SymbolTable::load()?);
let make_service = hyper::service::make_service_fn(move |_conn| {
let symbol_table = Arc::clone(&symbol_table);
let service = hyper::service::service_fn(move |req| handle(Arc::clone(&symbol_table), req));
async move { Ok::<_, io::Error>(service) }
});
let addr = SocketAddr::from(([127, 0, 0, 1], 12345));
tokio::spawn(async move { hyper::Server::bind(&addr).serve(make_service).await });
std::thread::Builder::new().name("pprof".to_string()).spawn(move || {
mallctl::set_thread_active(false).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let make_service = hyper::service::make_service_fn(move |_conn| {
let service = hyper::service::service_fn(move |req| handle(req));
async move { Ok::<_, io::Error>(service) }
});
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
if let Err(e) =
rt.block_on(async move { hyper::Server::bind(&addr).serve(make_service).await })
{
Err(io::Error::new(io::ErrorKind::Other, e))
} else {
Ok(())
}
})?;
```

```rust
async fn handle(
symbol_table: Arc<jeprof::SymbolTable>,
req: hyper::Request<hyper::Body>,
) -> io::Result<hyper::Response<hyper::Body>> {
async fn handle(req: hyper::Request<hyper::Body>) -> io::Result<hyper::Response<hyper::Body>> {
let (parts, body) = req.into_parts();
let body = hyper::body::to_bytes(body).await?;
let body =
hyper::body::to_bytes(body).await.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let req = hyper::Request::from_parts(parts, body.into());

let resp = jeprof::router(symbol_table.as_ref(), req)?;
let resp = jeprof::router(req).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;

let (parts, body) = resp.into_parts();
let body = hyper::Body::from(body);
let resp = hyper::Response::from_parts(parts, body);
let resp = resp.map(hyper::Body::from);

Ok(resp)
}
Expand All @@ -51,9 +58,10 @@ strip = false
```

Disable ASLR if necessary or install the `disable_aslr` helper tool.
On Linux you should prefer `setarch -R`.

```shell
cargo install microchassis
cargo install microchassis --features=disable_aslr
```

Use package manager to install `jeprof` or download from
Expand Down
4 changes: 2 additions & 2 deletions microchassis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ exclude = [".gitignore", ".github", "target"]
rust-version = "1.65"

[dependencies]
backtrace = { version = "0.3", optional = true }
http = "0.2"
lazy_static = "1"
libc = { version = "0.2", optional = true }
rustc-demangle = "0.1"
tempfile = "3"
thiserror = "1"
tikv-jemalloc-ctl = "0.5"
Expand All @@ -24,7 +24,7 @@ tracing = { version = "0.1" }
[features]
default = ["std", "jemalloc-profiling", "set-jemalloc-global"]
std = ["tikv-jemalloc-ctl/use_std"]
jemalloc-profiling = []
jemalloc-profiling = ["dep:backtrace"]
oompanic-allocator = []
set-jemalloc-global = []
disable_aslr = ["dep:libc"]
Expand Down
178 changes: 19 additions & 159 deletions microchassis/src/profiling/jeprof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@
use crate::profiling::mallctl;
use http::{header, Method, Request, Response, StatusCode};
use std::{env, fs::File, io, num::ParseIntError, process::Command};
use std::env;

#[inline]
pub fn router(sym: &SymbolTable, req: Request<Vec<u8>>) -> http::Result<Response<Vec<u8>>> {
pub fn router(req: Request<Vec<u8>>) -> http::Result<Response<Vec<u8>>> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/pprof/conf") => get_pprof_conf_handler(req),
(&Method::POST, "/pprof/conf") => post_pprof_conf_handler(req),
(&Method::GET, "/pprof/heap") => get_pprof_heap_handler(req),
(&Method::GET, "/pprof/cmdline") => get_pprof_cmdline_handler(req),
(&Method::GET, "/pprof/symbol") => get_pprof_symbol_handler(sym, req),
(&Method::POST, "/pprof/symbol") => post_pprof_symbol_handler(sym, req),
(&Method::GET, "/pprof/symbol") => get_pprof_symbol_handler(req),
(&Method::POST, "/pprof/symbol") => post_pprof_symbol_handler(req),
(&Method::GET, "/pprof/stats") => get_pprof_stats_handler(req),
_ => {
let body = b"Bad Request\r\n";
Expand Down Expand Up @@ -128,27 +128,29 @@ pub fn get_pprof_cmdline_handler(_req: Request<Vec<u8>>) -> http::Result<Respons

/// HTTP handler for GET /pprof/symbol.
#[inline]
pub fn get_pprof_symbol_handler(
sym: &SymbolTable,
_req: Request<Vec<u8>>,
) -> http::Result<Response<Vec<u8>>> {
let num_symbols = sym.len();
let body = format!("num_symbols: {num_symbols}\r\n");
response_ok(body.into_bytes())
pub fn get_pprof_symbol_handler(_req: Request<Vec<u8>>) -> http::Result<Response<Vec<u8>>> {
// TODO: any quick way to check if binary is stripped?
let body = b"num_symbols: 1\r\n";
response_ok(body.to_vec())
}

/// HTTP handler for POST /pprof/symbol.
#[inline]
pub fn post_pprof_symbol_handler(
sym: &SymbolTable,
req: Request<Vec<u8>>,
) -> http::Result<Response<Vec<u8>>> {
pub fn post_pprof_symbol_handler(req: Request<Vec<u8>>) -> http::Result<Response<Vec<u8>>> {
fn lookup_symbol(addr: u64) -> Option<String> {
let mut s: Option<String> = None;
backtrace::resolve(addr as *mut _, |symbol| {
s = symbol.name().map(|n| n.to_string());
});
s
}

let body = String::from_utf8_lossy(req.body());
let addrs = body
.split('+')
.filter_map(|addr| u64::from_str_radix(addr.trim_start_matches("0x"), 16).ok())
.map(|addr| (addr, sym.lookup_symbol(addr)))
.filter_map(|(addr, sym)| sym.map(|(_, sym)| (addr, sym)));
.map(|addr| (addr, lookup_symbol(addr)))
.filter_map(|(addr, sym)| sym.map(|sym| (addr, sym)));

let mut body = String::new();
for (addr, sym) in addrs {
Expand Down Expand Up @@ -207,145 +209,3 @@ fn response_err(msg: &str) -> http::Result<Response<Vec<u8>>> {
.header(header::CONTENT_LENGTH, msg.len())
.body(msg.as_bytes().to_owned())
}

#[derive(Default, Debug)]
pub struct SymbolTable {
sym: Vec<(u64, String)>,
vstart: u64,
vend: u64,
fstart: u64,
}

impl SymbolTable {
#[inline]
pub fn load() -> io::Result<Self> {
let nm_output = run_nm()?;
let (vstart, vend, fstart) = Self::load_mapping()?;
let mut sym = SymbolTable { sym: Vec::default(), vstart, vend, fstart };
sym.read_nm(nm_output.as_ref())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
Ok(sym)
}

fn load_mapping() -> io::Result<(u64, u64, u64)> {
#[cfg(target_os = "linux")]
{
use std::io::Read;

// TODO: clean up maps parsing, store all exec mappings
let exepath = env::current_exe()?;
let mut f = File::open("/proc/self/maps")?;
let mut buf = String::with_capacity(4096);
f.read_to_string(&mut buf)?;
for line in buf.lines() {
let parts: Vec<_> = line.splitn(6, ' ').map(str::trim).collect();
if parts.len() < 6 {
continue;
}
if parts[5] == exepath.to_string_lossy() && parts[1] == "r-xp" {
let addr_range: Vec<_> = parts[0]
.splitn(2, '-')
.filter_map(|n| u64::from_str_radix(n, 16).ok())
.collect();
if addr_range.len() != 2 {
continue;
}
let file_offset = u64::from_str_radix(parts[2], 16)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;

return Ok((addr_range[0], addr_range[1], file_offset));
}
}
}

Ok((u64::MAX, u64::MAX, 0))
}

fn read_nm(&mut self, output: &[u8]) -> Result<(), ParseIntError> {
use std::io::prelude::*;

let b = io::Cursor::new(output);
for line in b.lines() {
let line = line.expect("no I/O, no panic");
let parts: Vec<_> = line.split_ascii_whitespace().collect();
if parts.len() < 3 || parts[0] == "U" {
continue;
}
if parts[1] != "t" && parts[1] != "T" {
continue;
}

let address = u64::from_str_radix(parts[0].trim_start_matches("0x"), 16)?;
let symbol: String = parts[2..].join(" ");
let symbol = rustc_demangle::demangle(symbol.as_str());

self.sym.push((address, symbol.to_string()));
}

self.sym.sort();

Ok(())
}

#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.sym.len()
}

#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.sym.is_empty()
}

#[must_use]
#[inline]
pub fn lookup_symbol(&self, addr: u64) -> Option<&(u64, String)> {
let lookup_addr = if addr >= self.vstart && addr < self.vend {
addr - self.vstart + self.fstart
} else {
addr
};

match self.sym.binary_search_by_key(&lookup_addr, |(saddr, _)| *saddr) {
Ok(index) => self.sym.get(index),
Err(index) => {
if index == 0 {
None
} else {
self.sym.get(index - 1)
}
}
}
}
}

fn run_nm() -> io::Result<Vec<u8>> {
let exepath = env::current_exe()?;
let output =
Command::new("nm").args(["--numeric-sort", "--no-demangle"]).arg(exepath).output()?;
Ok(output.stdout)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_symtab_lookup_symbol() {
let symtab = SymbolTable {
sym: vec![(123, "Abc".to_string()), (456, "Def".to_string()), (789, "Xyz".to_string())],
vstart: 0,
vend: u64::MAX,
fstart: 0,
};

assert_eq!(None, symtab.lookup_symbol(100));
assert_eq!(Some(&(123, "Abc".to_string())), symtab.lookup_symbol(123));
assert_eq!(Some(&(123, "Abc".to_string())), symtab.lookup_symbol(200));
assert_eq!(Some(&(123, "Abc".to_string())), symtab.lookup_symbol(455));
assert_eq!(Some(&(456, "Def".to_string())), symtab.lookup_symbol(456));
assert_eq!(Some(&(789, "Xyz".to_string())), symtab.lookup_symbol(800));
}
}

0 comments on commit efb84fc

Please sign in to comment.