From 0e83aefa264f3dbe45c20c9a6e22c0e4137f644b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Quentin?= Date: Fri, 26 Sep 2025 14:12:28 +0200 Subject: [PATCH 1/3] A minimal stack-dump decoder (RV) --- Cargo.lock | 2 + espflash/Cargo.toml | 1 + espflash/src/cli/monitor/mod.rs | 1 + espflash/src/cli/monitor/parser/mod.rs | 20 ++- espflash/src/cli/monitor/stack_dump.rs | 204 +++++++++++++++++++++++++ 5 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 espflash/src/cli/monitor/stack_dump.rs diff --git a/Cargo.lock b/Cargo.lock index 7ab1606d..152aaf23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,6 +932,7 @@ dependencies = [ "env_logger", "esp-idf-part", "flate2", + "gimli 0.32.3", "indicatif", "libc", "log", @@ -1120,6 +1121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", + "indexmap", "stable_deref_trait", ] diff --git a/espflash/Cargo.toml b/espflash/Cargo.toml index 090f8e3f..3c2e0990 100644 --- a/espflash/Cargo.toml +++ b/espflash/Cargo.toml @@ -44,6 +44,7 @@ directories = { version = "6.0", optional = true } env_logger = { version = "0.11", optional = true } esp-idf-part = "0.6" flate2 = "1.1" +gimli = "0.32.3" indicatif = { version = "0.18", optional = true } log = "0.4" md-5 = "0.10" diff --git a/espflash/src/cli/monitor/mod.rs b/espflash/src/cli/monitor/mod.rs index 0ef15040..286fbe4d 100644 --- a/espflash/src/cli/monitor/mod.rs +++ b/espflash/src/cli/monitor/mod.rs @@ -39,6 +39,7 @@ pub mod external_processors; pub mod parser; mod line_endings; +mod stack_dump; mod symbols; /// Log format to use when parsing incoming data. diff --git a/espflash/src/cli/monitor/parser/mod.rs b/espflash/src/cli/monitor/parser/mod.rs index 47c63950..083cbdcc 100644 --- a/espflash/src/cli/monitor/parser/mod.rs +++ b/espflash/src/cli/monitor/parser/mod.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use regex::Regex; -use crate::cli::monitor::{line_endings::normalized, symbols::Symbols}; +use crate::cli::monitor::{line_endings::normalized, stack_dump, symbols::Symbols}; pub mod esp_defmt; pub mod serial; @@ -109,6 +109,7 @@ impl Utf8Merger { pub struct ResolvingPrinter<'ctx, W: Write> { writer: W, symbols: Option>, + elf: Option<&'ctx [u8]>, merger: Utf8Merger, line_fragment: String, disable_address_resolution: bool, @@ -120,6 +121,7 @@ impl<'ctx, W: Write> ResolvingPrinter<'ctx, W> { Self { writer, symbols: elf.and_then(|elf| Symbols::try_from(elf).ok()), + elf, merger: Utf8Merger::new(), line_fragment: String::new(), disable_address_resolution: false, @@ -131,6 +133,7 @@ impl<'ctx, W: Write> ResolvingPrinter<'ctx, W> { Self { writer, symbols: None, // Don't load symbols when address resolution is disabled + elf: None, merger: Utf8Merger::new(), line_fragment: String::new(), disable_address_resolution: true, @@ -177,6 +180,21 @@ impl Write for ResolvingPrinter<'_, W> { // Try to print the names of addresses in the current line. resolve_addresses(symbols, &line, &mut self.writer)?; } + + if line.starts_with(stack_dump::MARKER) { + if let Some(symbols) = self.symbols.as_ref() { + if stack_dump::backtrace_from_stack_dump( + &line, + &mut self.writer, + self.elf, + symbols, + ) + .is_err() + { + self.writer.queue(Print("\nUnable to decode stack-dump. Double check `-Cforce-unwind-tables` is used.\n"))?; + } + } + } } } diff --git a/espflash/src/cli/monitor/stack_dump.rs b/espflash/src/cli/monitor/stack_dump.rs new file mode 100644 index 00000000..2024fcd1 --- /dev/null +++ b/espflash/src/cli/monitor/stack_dump.rs @@ -0,0 +1,204 @@ +use std::{collections::HashMap, io::Write, rc::Rc}; + +use gimli::{Section, UnwindSection}; +use object::{Object, ObjectSection}; + +use crate::cli::monitor::symbols::Symbols; + +pub(crate) const MARKER: &str = "STACKDUMP: "; + +pub(crate) fn backtrace_from_stack_dump( + line: &str, + out: &mut dyn Write, + elf: Option<&[u8]>, + symbols: &Symbols<'_>, +) -> std::io::Result<()> { + if let Some(elf) = elf { + if let Some(remaining) = line.to_string().strip_prefix(MARKER) { + let mut split = remaining.split(" "); + let (address, stack) = { + let first = split.next(); + let second = split.next(); + + (first, second) + }; + + if let Some(address) = address { + if let Some(stack) = stack { + if stack.len() % 2 != 0 { + return Ok(()); + } + + let mut pc = u32::from_str_radix(address, 16).unwrap_or_default(); + let mut stack_bytes = Vec::new(); + for byte_chars in stack.chars().collect::>().chunks(2) { + if byte_chars.len() == 2 { + stack_bytes.push( + u8::from_str_radix( + &format!("{}{}", byte_chars[0], byte_chars[1]), + 16, + ) + .unwrap_or_default(), + ); + } + } + + let func_info = get_func_info(elf)?; + + writeln!(out).ok(); + let mut index = 0; + loop { + let func = func_info.iter().find(|f| f.start <= pc && f.end >= pc); + if let Some(func) = func { + if func.stack_frame_size == 0 { + break; + } + + let lookup_pc = pc as u64 - 4; + let name = symbols.name(lookup_pc); + let location = symbols.location(lookup_pc); + if let Some(name) = name { + if let Some((file, line_num)) = location { + writeln!(out, "{name}\r\n at {file}:{line_num}\r\n").ok(); + } else { + writeln!(out, "{name}\r\n at ??:??\r\n").ok(); + } + } + + if index + func.stack_frame_size as usize > stack_bytes.len() { + break; + } + + let next_pc_pos = index + (func.stack_frame_size as usize - 4); + + pc = u32::from_le_bytes( + stack_bytes[next_pc_pos..][..4] + .try_into() + .unwrap_or_default(), + ); + index += func.stack_frame_size as usize; + } else { + break; + } + } + writeln!(out).ok(); + } + } + } + } + + Ok(()) +} + +fn get_func_info(elf: &[u8]) -> Result, std::io::Error> { + let debug_file = object::File::parse(elf).expect("parse file"); + + let endian = if debug_file.is_little_endian() { + gimli::RunTimeEndian::Little + } else { + gimli::RunTimeEndian::Big + }; + + let eh_frame = gimli::EhFrame::load(|sect_id| { + let data = debug_file + .section_by_name(sect_id.name()) + .and_then(|section| section.data().ok()); + + if let Some(data) = data { + Ok::>, ()>( + gimli::EndianRcSlice::new(Rc::from(data), endian), + ) + } else { + Err(()) + } + }) + .map_err(|_| std::io::Error::other("no eh_frame section"))?; + + process_eh_frame(&debug_file, eh_frame).map_err(|_| std::io::Error::other("eh_frame error")) +} + +#[derive(Debug)] +struct FuncInfo { + start: u32, + end: u32, + stack_frame_size: u32, +} + +fn process_eh_frame>( + file: &object::File<'_>, + mut eh_frame: gimli::EhFrame, +) -> Result, gimli::Error> { + let mut res = Vec::new(); + + let address_size = file + .architecture() + .address_size() + .map(|w| w.bytes()) + .unwrap_or(std::mem::size_of::() as u8); + eh_frame.set_address_size(address_size); + + let mut bases = gimli::BaseAddresses::default(); + if let Some(section) = file.section_by_name(".eh_frame") { + bases = bases.set_eh_frame(section.address()); + } + + let mut cies = HashMap::new(); + + let mut entries = eh_frame.entries(&bases); + loop { + match entries.next()? { + None => return Ok(res), + Some(gimli::CieOrFde::Fde(partial)) => { + let fde = match partial.parse(|_, bases, o| { + cies.entry(o) + .or_insert_with(|| eh_frame.cie_from_offset(bases, o)) + .clone() + }) { + Ok(fde) => fde, + Err(_) => { + // ignored + continue; + } + }; + + let mut entry = FuncInfo { + start: fde.initial_address() as u32, + end: fde.end_address() as u32, + stack_frame_size: 0u32, + }; + + let instructions = fde.instructions(&eh_frame, &bases); + let sfs = estimate_stack_frame_size(instructions)?; + entry.stack_frame_size = sfs; + res.push(entry); + } + _ => (), + } + } +} + +fn estimate_stack_frame_size( + mut insns: gimli::CallFrameInstructionIter<'_, R>, +) -> Result { + use gimli::CallFrameInstruction::*; + + let mut sfs = 0; + + loop { + match insns.next() { + Err(_e) => { + break; + } + Ok(None) => { + break; + } + Ok(Some(op)) => { + if let DefCfaOffset { offset } = op { + sfs = u32::max(sfs, offset as u32); + } + } + } + } + + Ok(sfs) +} From e6161f35dfe379ce02d4d83e6ddc4de0d5a52f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Quentin?= Date: Fri, 26 Sep 2025 14:16:36 +0200 Subject: [PATCH 2/3] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5fcfbe5..667adcdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add chip detection based on security info, where supported (#953) +- Support for decoding esp-backtrace's stack-dump output (#955) ### Changed - Moved `SecurityInfo` to the `connection` module from the `flasher` module (#953) From 2ede94eccffd0fc4ba65a66f02e9a74e377744aa Mon Sep 17 00:00:00 2001 From: Sergio Gasquez Arcos Date: Wed, 1 Oct 2025 10:18:54 +0200 Subject: [PATCH 3/3] docs: Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 667adcdb..b5b65d9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add chip detection based on security info, where supported (#953) -- Support for decoding esp-backtrace's stack-dump output (#955) +- Support for decoding `esp-backtrace`'s RISC-V stack-dump output (#955) ### Changed - Moved `SecurityInfo` to the `connection` module from the `flasher` module (#953)