diff --git a/.travis.yml b/.travis.yml index c56985a7..a5a769d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,28 @@ language: rust + rust: - stable - beta - nightly + sudo: false + +dist: trusty + +addons: + sources: + # Provides clang-3.9 + - llvm-toolchain-trusty-3.9 + apt: + packages: + # Required for `bindgen`, which is required by `findshlibs`, which is + # required by the `gimli` feature. + - clang-3.9 + before_script: - pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH + - export LIBCLANG_PATH=/usr/lib/llvm-3.9/lib + script: - cargo test - cargo test --no-default-features @@ -19,15 +36,19 @@ script: - cargo test --no-default-features --features 'serialize-rustc' - cargo test --no-default-features --features 'serialize-rustc serialize-serde' - cargo test --no-default-features --features 'cpp_demangle' + - cargo test --no-default-features --features 'gimli-symbolize' - cd ./cpp_smoke_test && cargo test && cd .. - cargo clean && cargo build - rustdoc --test README.md -L target/debug/deps -L target/debug - cargo doc --no-deps + notifications: email: on_success: never + after_success: - travis-cargo --only nightly doc-upload + env: global: # serde-codegen has historically needed a large stack to expand diff --git a/Cargo.toml b/Cargo.toml index f23135e5..12d4168f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,9 @@ rustc-serialize = { version = "0.3", optional = true } # Optionally demangle C++ frames' symbols in backtraces. cpp_demangle = { default-features = false, version = "0.2.3", optional = true } +addr2line = { version = "0.5.0", optional = true } +findshlibs = { version = "0.3.3", optional = true } + [target.'cfg(windows)'.dependencies] dbghelp-sys = { version = "0.2", optional = true } kernel32-sys = { version = "0.2", optional = true } @@ -75,9 +78,15 @@ default = ["libunwind", "libbacktrace", "coresymbolication", "dladdr", "dbghelp" # enough on OSX. # - coresymbolication: this feature uses the undocumented core symbolication # framework on OS X to symbolize. + # - gimli-symbolize: use the `gimli-rs/addr2line` crate to symbolicate + # addresses into file, line, and name using DWARF debug information. At + # the moment, this is only possible when targetting Linux, since macOS + # splits DWARF out into a separate object file. Enabling this feature + # means one less C dependency. libbacktrace = ["backtrace-sys"] dladdr = [] coresymbolication = [] + gimli-symbolize = ["addr2line", "findshlibs"] #======================================= # Methods of serialization diff --git a/src/lib.rs b/src/lib.rs index 56251b1a..4c547a5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,6 +92,15 @@ extern crate rustc_demangle; #[cfg(feature = "cpp_demangle")] extern crate cpp_demangle; +#[cfg(all(feature = "gimli-symbolize", + unix, + target_os = "linux"))] +extern crate addr2line; +#[cfg(all(feature = "gimli-symbolize", + unix, + target_os = "linux"))] +extern crate findshlibs; + #[allow(dead_code)] // not used everywhere #[cfg(unix)] #[macro_use] diff --git a/src/symbolize/gimli.rs b/src/symbolize/gimli.rs new file mode 100644 index 00000000..5ee71505 --- /dev/null +++ b/src/symbolize/gimli.rs @@ -0,0 +1,170 @@ +use addr2line; +use findshlibs::{self, Segment, SharedLibrary}; +use std::cell::RefCell; +use std::env; +use std::os::raw::c_void; +use std::path::{Path, PathBuf}; +use std::u32; + +use SymbolName; + +const MAPPINGS_CACHE_SIZE: usize = 4; + +thread_local! { + // A very small, very simple LRU cache for debug info mappings. + // + // The hit rate should be very high, since the typical stack doesn't cross + // between many shared libraries. + // + // The `addr2line::Mapping` structures are pretty expensive to create. Its + // cost is expected to be amortized by subsequent `locate` queries, which + // leverage the structures built when constructing `addr2line::Mapping`s to + // get nice speedups. If we didn't have this cache, that amortization would + // never happen, and symbolicating backtraces would be ssssllllooooowwww. + static MAPPINGS_CACHE: RefCell> + = RefCell::new(Vec::with_capacity(MAPPINGS_CACHE_SIZE)); +} + +fn with_mapping_for_path(path: PathBuf, mut f: F) +where + F: FnMut(&mut addr2line::Mapping) +{ + MAPPINGS_CACHE.with(|cache| { + let mut cache = cache.borrow_mut(); + + let idx = cache.iter().position(|&(ref p, _)| p == &path); + + // Invariant: after this conditional completes without early returning + // from an error, the cache entry for this path is at index 0. + + if let Some(idx) = idx { + // When the mapping is already in the cache, move it to the front. + if idx != 0 { + let entry = cache.remove(idx); + cache.insert(0, entry); + } + } else { + // When the mapping is not in the cache, create a new mapping, + // insert it into the front of the cache, and evict the oldest cache + // entry if necessary. + let opts = addr2line::Options::default() + .with_functions(); + + let mapping = match opts.build(&path) { + Err(_) => return, + Ok(m) => m, + }; + + if cache.len() == MAPPINGS_CACHE_SIZE { + cache.pop(); + } + + cache.insert(0, (path, mapping)); + } + + f(&mut cache[0].1); + }); +} + +pub fn resolve(addr: *mut c_void, cb: &mut FnMut(&super::Symbol)) { + // First, find the file containing the segment that the given AVMA (after + // relocation) address falls within. Use the containing segment to compute + // the SVMA (before relocation) address. + // + // Note that the OS APIs that `SharedLibrary::each` is implemented with hold + // a lock for the duration of the `each` call, so we want to keep this + // section as short as possible to avoid contention with other threads + // capturing backtraces. + let addr = findshlibs::Avma(addr as *mut u8 as *const u8); + let mut so_info = None; + findshlibs::TargetSharedLibrary::each(|so| { + use findshlibs::IterationControl::*; + + for segment in so.segments() { + if segment.contains_avma(so, addr) { + let addr = so.avma_to_svma(addr); + let path = so.name().to_string_lossy(); + so_info = Some((addr, path.to_string())); + return Break; + } + } + + Continue + }); + let (addr, path) = match so_info { + None => return, + Some((a, p)) => (a, p), + }; + + // Second, fixup the path. Empty path means that this address falls within + // the main executable, not a shared library. + let path = if path.is_empty() { + match env::current_exe() { + Err(_) => return, + Ok(p) => p, + } + } else { + PathBuf::from(path) + }; + + // Finally, get a cached mapping or create a new mapping for this file, and + // evaluate the DWARF info to find the file/line/name for this address. + with_mapping_for_path(path, |mapping| { + let (file, line, func) = match mapping.locate(addr.0 as u64) { + Ok(None) | Err(_) => return, + Ok(Some((file, line, func))) => (file, line, func), + }; + + let sym = super::Symbol { + inner: Symbol::new(addr.0 as usize, + file, + line, + func.map(|f| f.to_string())) + }; + + cb(&sym); + }); +} + +pub struct Symbol { + addr: usize, + file: PathBuf, + line: Option, + name: Option, +} + +impl Symbol { + fn new(addr: usize, + file: PathBuf, + line: Option, + name: Option) + -> Symbol { + Symbol { + addr, + file, + line, + name, + } + } + + pub fn name(&self) -> Option { + self.name.as_ref().map(|s| SymbolName::new(s.as_bytes())) + } + + pub fn addr(&self) -> Option<*mut c_void> { + Some(self.addr as *mut c_void) + } + + pub fn filename(&self) -> Option<&Path> { + Some(self.file.as_ref()) + } + + pub fn lineno(&self) -> Option { + self.line + .and_then(|l| if l > (u32::MAX as u64) { + None + } else { + Some(l as u32) + }) + } +} diff --git a/src/symbolize/mod.rs b/src/symbolize/mod.rs index 27f6b44a..7ed65479 100644 --- a/src/symbolize/mod.rs +++ b/src/symbolize/mod.rs @@ -255,6 +255,12 @@ cfg_if! { mod dbghelp; use self::dbghelp::resolve as resolve_imp; use self::dbghelp::Symbol as SymbolImp; + } else if #[cfg(all(feature = "gimli-symbolize", + unix, + target_os = "linux"))] { + mod gimli; + use self::gimli::resolve as resolve_imp; + use self::gimli::Symbol as SymbolImp; } else if #[cfg(all(feature = "libbacktrace", unix, not(target_os = "emscripten"), diff --git a/tests/smoke.rs b/tests/smoke.rs index bef71af3..1ec6f2b1 100644 --- a/tests/smoke.rs +++ b/tests/smoke.rs @@ -12,6 +12,9 @@ static CORESYMBOLICATION: bool = cfg!(all(any(target_os = "macos", target_os = " static DLADDR: bool = cfg!(all(unix, feature = "dladdr")); static DBGHELP: bool = cfg!(all(windows, feature = "dbghelp")); static MSVC: bool = cfg!(target_env = "msvc"); +static GIMLI_SYMBOLIZE: bool = cfg!(all(feature = "gimli-symbolize", + unix, + target_os = "linux")); #[test] fn smoke_test_frames() { @@ -71,7 +74,7 @@ fn smoke_test_frames() { } let mut resolved = 0; - let can_resolve = DLADDR || LIBBACKTRACE || CORESYMBOLICATION || DBGHELP; + let can_resolve = DLADDR || LIBBACKTRACE || CORESYMBOLICATION || DBGHELP || GIMLI_SYMBOLIZE; let mut name = None; let mut addr = None;