diff --git a/Cargo.lock b/Cargo.lock index 03ccdf34..76846faf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,19 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" +dependencies = [ + "cpp_demangle", + "fallible-iterator", + "gimli 0.22.0", + "object 0.20.0", + "rustc-demangle", + "smallvec", +] + [[package]] name = "adler" version = "0.2.3" @@ -180,6 +194,16 @@ dependencies = [ "syn", ] +[[package]] +name = "cpp_demangle" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad49cad3673b9d586bc50cd92fdc0e9205ecd162d4206073d9774c4fe13a8fde" +dependencies = [ + "cfg-if", + "glob", +] + [[package]] name = "crc32fast" version = "1.2.0" @@ -341,6 +365,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "goblin" version = "0.2.3" @@ -516,10 +546,6 @@ name = "object" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37fd5004feb2ce328a52b0b3d01dbf4ffff72583493900ed15f22d4111c51693" -dependencies = [ - "flate2", - "wasmparser", -] [[package]] name = "panic-probe" @@ -602,6 +628,7 @@ dependencies = [ name = "probe-run" version = "0.1.3" dependencies = [ + "addr2line", "anyhow", "arrayref", "colored", @@ -609,7 +636,7 @@ dependencies = [ "defmt-elf2table", "gimli 0.22.0", "log", - "object 0.21.1", + "object 0.20.0", "probe-rs", "probe-rs-rtt", "rustc-demangle", @@ -804,6 +831,12 @@ dependencies = [ "libc", ] +[[package]] +name = "smallvec" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 1e8fe1f2..1d32531f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,19 +11,21 @@ repository = "https://github.com/knurling-rs/probe-run" version = "0.1.3" [dependencies] +addr2line = "0.13.0" anyhow = "1.0.32" arrayref = "0.3.6" colored = "2.0.0" -signal-hook = "0.1.16" defmt-decoder = { git = "https://github.com/knurling-rs/defmt", branch = "main", optional = true } defmt-elf2table = { git = "https://github.com/knurling-rs/defmt", branch = "main", optional = true } gimli = "0.22.0" log = { version = "0.4.11", features = ["std"] } +# an addr2line trait is implement for a type in this particular version +object = "0.20.0" probe-rs = "0.8.0" probe-rs-rtt = "0.3.0" rustc-demangle = "0.1.16" +signal-hook = "0.1.16" structopt = "0.3.15" -object = "0.21.1" [features] defmt = ["defmt-elf2table", "defmt-decoder"] diff --git a/src/main.rs b/src/main.rs index a5bcaf0c..68ae63cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,19 +4,20 @@ use core::{ cmp, convert::TryInto, mem, - ops::Range, sync::atomic::{AtomicBool, Ordering}, }; use std::{ - collections::{btree_map, BTreeMap}, + borrow::Cow, + collections::{btree_map, BTreeMap, HashSet}, fs, io::{self, Write as _}, - path::PathBuf, + path::{Path, PathBuf}, process, sync::{Arc, Mutex}, time::Duration, }; +use addr2line::fallible_iterator::FallibleIterator as _; use anyhow::{anyhow, bail, Context}; use arrayref::array_ref; use colored::Colorize as _; @@ -26,7 +27,7 @@ use gimli::{ }; use object::{ read::{File as ElfFile, Object as _, ObjectSection as _}, - SectionIndex, + SymbolSection, }; use probe_rs::config::{registry, MemoryRegion, RamRegion}; use probe_rs::{ @@ -123,12 +124,16 @@ fn notmain() -> Result<i32, anyhow::Error> { ); } - let text = elf.section_by_name(".text").ok_or_else(|| { - anyhow!( + // NOTE we want to raise the linking error before calling `defmt_elf2table::parse` + let text = elf + .section_by_name(".text") + .map(|section| section.index()) + .ok_or_else(|| { + anyhow!( "`.text` section is missing, please make sure that the linker script was passed to the \ linker (check `.cargo/config.toml` and the `RUSTFLAGS` variable)" ) - })?; + })?; #[cfg(feature = "defmt")] let (table, locs) = { @@ -228,7 +233,20 @@ fn notmain() -> Result<i32, anyhow::Error> { } } - let (range_names, rtt_addr, uses_heap) = range_names_from(&elf, text.index())?; + let live_functions = elf + .symbol_map() + .symbols() + .iter() + .filter_map(|sym| { + if sym.section() == SymbolSection::Section(text) { + sym.name() + } else { + None + } + }) + .collect::<HashSet<_>>(); + + let (rtt_addr, uses_heap) = rtt_and_heap_info_from(&elf)?; let vector_table = vector_table.ok_or_else(|| anyhow!("`.vector_table` section is missing"))?; log::debug!("vector table: {:x?}", vector_table); @@ -338,7 +356,6 @@ fn notmain() -> Result<i32, anyhow::Error> { #[cfg(feature = "defmt")] let mut frames = vec![]; let mut was_halted = false; - #[cfg(feature = "defmt")] let current_dir = std::env::current_dir()?; // TODO strip prefix from crates-io paths (?) while !exit.load(Ordering::Relaxed) { @@ -454,9 +471,11 @@ fn notmain() -> Result<i32, anyhow::Error> { &mut core, pc, debug_frame, - &range_names, + &elf, &vector_table, &sp_ram_region, + &live_functions, + ¤t_dir, )?; core.reset_and_halt(TIMEOUT)?; @@ -598,9 +617,11 @@ fn backtrace( core: &mut Core<'_>, mut pc: u32, debug_frame: &[u8], - range_names: &RangeNames, + elf: &ElfFile, vector_table: &VectorTable, sp_ram_region: &Option<RamRegion>, + live_functions: &HashSet<&str>, + current_dir: &Path, ) -> Result<Option<TopException>, anyhow::Error> { let mut debug_frame = DebugFrame::new(debug_frame, LittleEndian); // 32-bit ARM -- this defaults to the host's address size which is likely going to be 8 @@ -613,24 +634,71 @@ fn backtrace( let bases = &BaseAddresses::default(); let ctx = &mut UninitializedUnwindContext::new(); + let addr2line = addr2line::Context::new(elf)?; let mut top_exception = None; - let mut frame = 0; + let mut frame_index = 0; let mut registers = Registers::new(lr, sp, core); + let symtab = elf.symbol_map(); println!("stack backtrace:"); loop { - let name = range_names - .binary_search_by(|rn| { - if rn.0.contains(&pc) { - cmp::Ordering::Equal - } else if pc < rn.0.start { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less + let frames = addr2line.find_frames(pc as u64)?.collect::<Vec<_>>()?; + + // `find_frames` returns a wrong answer, instead of an `Err`or, when the input is the PC of + // a subroutine that has no debug information (e.g. external assembly). The wrong answer is + // one of the subroutines GC-ed by the linker so we check that the last frame + // (the non-inline one) is actually "live" (exists in the final binary). If it doesn't then + // we probably asked about something with no debug info. In that scenario we fallback to + // the symtab to at least provide the function name that contains the PC. + let subroutine = frames + .last() + .expect("BUG: `addr2line::FrameIter` was empty"); + let has_valid_debuginfo = if let Some(function) = subroutine.function.as_ref() { + live_functions.contains(&*function.raw_name()?) + } else { + false + }; + + if has_valid_debuginfo { + for frame in &frames { + let name = frame + .function + .as_ref() + .map(|function| function.demangle()) + .transpose()? + .unwrap_or(Cow::Borrowed("???")); + + println!("{:>4}: {}", frame_index, name); + frame_index += 1; + + if let Some((file, line)) = frame + .location + .as_ref() + .and_then(|loc| loc.file.and_then(|file| loc.line.map(|line| (file, line)))) + { + let file = Path::new(file); + let relpath = if let Ok(relpath) = file.strip_prefix(¤t_dir) { + relpath + } else { + // not within current directory; use full path + file + }; + println!(" at {}:{}", relpath.display(), line); } - }) - .map(|idx| &*range_names[idx].1) - .unwrap_or("<unknown>"); - println!("{:>4}: {:#010x} - {}", frame, pc, name); + } + } else { + // .symtab fallback + // the .symtab appears to use address ranges that have their thumb bits set (e.g. + // `0x101..0x200`). Passing the `pc` with the thumb bit cleared (e.g. `0x100`) to the + // lookup function sometimes returns the *previous* symbol. Work around the issue by + // setting `pc`'s thumb bit before looking it up + let address = (pc | THUMB_BIT) as u64; + let name = symtab + .get(address) + .and_then(|symbol| symbol.name()) + .unwrap_or("???"); + println!("{:>4}: {}", frame_index, name); + frame_index += 1; + } // on hard fault exception entry we hit the breakpoint before the subroutine prelude (`push // lr`) is executed so special handling is required @@ -708,8 +776,6 @@ fn backtrace( } pc = lr & !THUMB_BIT; } - - frame += 1; } Ok(top_exception) @@ -853,15 +919,10 @@ impl Stacked { num_words as u32 * 4 } } -// FIXME this might already exist in the DWARF data; we should just use that -/// Map from PC ranges to demangled Rust names -type RangeNames = Vec<(Range<u32>, String)>; -fn range_names_from( +fn rtt_and_heap_info_from( elf: &ElfFile, - text: SectionIndex, -) -> Result<(RangeNames, Option<u32>, bool /* uses heap */), anyhow::Error> { - let mut range_names = vec![]; +) -> Result<(Option<u32>, bool /* uses heap */), anyhow::Error> { let mut rtt = None; let mut uses_heap = false; @@ -879,29 +940,9 @@ fn range_names_from( } _ => {} } - - if symbol.section_index() == Some(text) && symbol.size() > 0 { - let mut name = rustc_demangle::demangle(name).to_string(); - // clear the thumb bit - let start = symbol.address() as u32 & !1; - - // strip the hash (e.g. `::hd881d91ced85c2b0`) - let hash_len = "::hd881d91ced85c2b0".len(); - if let Some(pos) = name.len().checked_sub(hash_len) { - let maybe_hash = &name[pos..]; - if maybe_hash.starts_with("::h") { - // FIXME avoid this allocation - name = name[..pos].to_string(); - } - } - - range_names.push((start..start + symbol.size() as u32, name)); - } } - range_names.sort_unstable_by(|a, b| a.0.start.cmp(&b.0.start)); - - Ok((range_names, rtt, uses_heap)) + Ok((rtt, uses_heap)) } const LR: CoreRegisterAddress = CoreRegisterAddress(14);