Skip to content

Commit 40adcd8

Browse files
A minimal stack-dump decoder (RV) (#955)
* A minimal stack-dump decoder (RV) * CHANGELOG * docs: Update CHANGELOG.md --------- Co-authored-by: Sergio Gasquez Arcos <sergio.gasquez@gmail.com>
1 parent f8a344f commit 40adcd8

File tree

6 files changed

+228
-1
lines changed

6 files changed

+228
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- Add chip detection based on security info, where supported (#953)
12+
- Support for decoding `esp-backtrace`'s RISC-V stack-dump output (#955)
1213

1314
### Changed
1415
- Moved `SecurityInfo` to the `connection` module from the `flasher` module (#953)

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

espflash/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ directories = { version = "6.0", optional = true }
4444
env_logger = { version = "0.11", optional = true }
4545
esp-idf-part = "0.6"
4646
flate2 = "1.1"
47+
gimli = "0.32.3"
4748
indicatif = { version = "0.18", optional = true }
4849
log = "0.4"
4950
md-5 = "0.10"

espflash/src/cli/monitor/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub mod external_processors;
3939
pub mod parser;
4040

4141
mod line_endings;
42+
mod stack_dump;
4243
mod symbols;
4344

4445
/// Log format to use when parsing incoming data.

espflash/src/cli/monitor/parser/mod.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crossterm::{
66
};
77
use regex::Regex;
88

9-
use crate::cli::monitor::{line_endings::normalized, symbols::Symbols};
9+
use crate::cli::monitor::{line_endings::normalized, stack_dump, symbols::Symbols};
1010

1111
pub mod esp_defmt;
1212
pub mod serial;
@@ -109,6 +109,7 @@ impl Utf8Merger {
109109
pub struct ResolvingPrinter<'ctx, W: Write> {
110110
writer: W,
111111
symbols: Option<Symbols<'ctx>>,
112+
elf: Option<&'ctx [u8]>,
112113
merger: Utf8Merger,
113114
line_fragment: String,
114115
disable_address_resolution: bool,
@@ -120,6 +121,7 @@ impl<'ctx, W: Write> ResolvingPrinter<'ctx, W> {
120121
Self {
121122
writer,
122123
symbols: elf.and_then(|elf| Symbols::try_from(elf).ok()),
124+
elf,
123125
merger: Utf8Merger::new(),
124126
line_fragment: String::new(),
125127
disable_address_resolution: false,
@@ -131,6 +133,7 @@ impl<'ctx, W: Write> ResolvingPrinter<'ctx, W> {
131133
Self {
132134
writer,
133135
symbols: None, // Don't load symbols when address resolution is disabled
136+
elf: None,
134137
merger: Utf8Merger::new(),
135138
line_fragment: String::new(),
136139
disable_address_resolution: true,
@@ -177,6 +180,21 @@ impl<W: Write> Write for ResolvingPrinter<'_, W> {
177180
// Try to print the names of addresses in the current line.
178181
resolve_addresses(symbols, &line, &mut self.writer)?;
179182
}
183+
184+
if line.starts_with(stack_dump::MARKER) {
185+
if let Some(symbols) = self.symbols.as_ref() {
186+
if stack_dump::backtrace_from_stack_dump(
187+
&line,
188+
&mut self.writer,
189+
self.elf,
190+
symbols,
191+
)
192+
.is_err()
193+
{
194+
self.writer.queue(Print("\nUnable to decode stack-dump. Double check `-Cforce-unwind-tables` is used.\n"))?;
195+
}
196+
}
197+
}
180198
}
181199
}
182200

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
use std::{collections::HashMap, io::Write, rc::Rc};
2+
3+
use gimli::{Section, UnwindSection};
4+
use object::{Object, ObjectSection};
5+
6+
use crate::cli::monitor::symbols::Symbols;
7+
8+
pub(crate) const MARKER: &str = "STACKDUMP: ";
9+
10+
pub(crate) fn backtrace_from_stack_dump(
11+
line: &str,
12+
out: &mut dyn Write,
13+
elf: Option<&[u8]>,
14+
symbols: &Symbols<'_>,
15+
) -> std::io::Result<()> {
16+
if let Some(elf) = elf {
17+
if let Some(remaining) = line.to_string().strip_prefix(MARKER) {
18+
let mut split = remaining.split(" ");
19+
let (address, stack) = {
20+
let first = split.next();
21+
let second = split.next();
22+
23+
(first, second)
24+
};
25+
26+
if let Some(address) = address {
27+
if let Some(stack) = stack {
28+
if stack.len() % 2 != 0 {
29+
return Ok(());
30+
}
31+
32+
let mut pc = u32::from_str_radix(address, 16).unwrap_or_default();
33+
let mut stack_bytes = Vec::new();
34+
for byte_chars in stack.chars().collect::<Vec<char>>().chunks(2) {
35+
if byte_chars.len() == 2 {
36+
stack_bytes.push(
37+
u8::from_str_radix(
38+
&format!("{}{}", byte_chars[0], byte_chars[1]),
39+
16,
40+
)
41+
.unwrap_or_default(),
42+
);
43+
}
44+
}
45+
46+
let func_info = get_func_info(elf)?;
47+
48+
writeln!(out).ok();
49+
let mut index = 0;
50+
loop {
51+
let func = func_info.iter().find(|f| f.start <= pc && f.end >= pc);
52+
if let Some(func) = func {
53+
if func.stack_frame_size == 0 {
54+
break;
55+
}
56+
57+
let lookup_pc = pc as u64 - 4;
58+
let name = symbols.name(lookup_pc);
59+
let location = symbols.location(lookup_pc);
60+
if let Some(name) = name {
61+
if let Some((file, line_num)) = location {
62+
writeln!(out, "{name}\r\n at {file}:{line_num}\r\n").ok();
63+
} else {
64+
writeln!(out, "{name}\r\n at ??:??\r\n").ok();
65+
}
66+
}
67+
68+
if index + func.stack_frame_size as usize > stack_bytes.len() {
69+
break;
70+
}
71+
72+
let next_pc_pos = index + (func.stack_frame_size as usize - 4);
73+
74+
pc = u32::from_le_bytes(
75+
stack_bytes[next_pc_pos..][..4]
76+
.try_into()
77+
.unwrap_or_default(),
78+
);
79+
index += func.stack_frame_size as usize;
80+
} else {
81+
break;
82+
}
83+
}
84+
writeln!(out).ok();
85+
}
86+
}
87+
}
88+
}
89+
90+
Ok(())
91+
}
92+
93+
fn get_func_info(elf: &[u8]) -> Result<Vec<FuncInfo>, std::io::Error> {
94+
let debug_file = object::File::parse(elf).expect("parse file");
95+
96+
let endian = if debug_file.is_little_endian() {
97+
gimli::RunTimeEndian::Little
98+
} else {
99+
gimli::RunTimeEndian::Big
100+
};
101+
102+
let eh_frame = gimli::EhFrame::load(|sect_id| {
103+
let data = debug_file
104+
.section_by_name(sect_id.name())
105+
.and_then(|section| section.data().ok());
106+
107+
if let Some(data) = data {
108+
Ok::<gimli::EndianReader<gimli::RunTimeEndian, Rc<[u8]>>, ()>(
109+
gimli::EndianRcSlice::new(Rc::from(data), endian),
110+
)
111+
} else {
112+
Err(())
113+
}
114+
})
115+
.map_err(|_| std::io::Error::other("no eh_frame section"))?;
116+
117+
process_eh_frame(&debug_file, eh_frame).map_err(|_| std::io::Error::other("eh_frame error"))
118+
}
119+
120+
#[derive(Debug)]
121+
struct FuncInfo {
122+
start: u32,
123+
end: u32,
124+
stack_frame_size: u32,
125+
}
126+
127+
fn process_eh_frame<R: gimli::Reader<Offset = usize>>(
128+
file: &object::File<'_>,
129+
mut eh_frame: gimli::EhFrame<R>,
130+
) -> Result<Vec<FuncInfo>, gimli::Error> {
131+
let mut res = Vec::new();
132+
133+
let address_size = file
134+
.architecture()
135+
.address_size()
136+
.map(|w| w.bytes())
137+
.unwrap_or(std::mem::size_of::<usize>() as u8);
138+
eh_frame.set_address_size(address_size);
139+
140+
let mut bases = gimli::BaseAddresses::default();
141+
if let Some(section) = file.section_by_name(".eh_frame") {
142+
bases = bases.set_eh_frame(section.address());
143+
}
144+
145+
let mut cies = HashMap::new();
146+
147+
let mut entries = eh_frame.entries(&bases);
148+
loop {
149+
match entries.next()? {
150+
None => return Ok(res),
151+
Some(gimli::CieOrFde::Fde(partial)) => {
152+
let fde = match partial.parse(|_, bases, o| {
153+
cies.entry(o)
154+
.or_insert_with(|| eh_frame.cie_from_offset(bases, o))
155+
.clone()
156+
}) {
157+
Ok(fde) => fde,
158+
Err(_) => {
159+
// ignored
160+
continue;
161+
}
162+
};
163+
164+
let mut entry = FuncInfo {
165+
start: fde.initial_address() as u32,
166+
end: fde.end_address() as u32,
167+
stack_frame_size: 0u32,
168+
};
169+
170+
let instructions = fde.instructions(&eh_frame, &bases);
171+
let sfs = estimate_stack_frame_size(instructions)?;
172+
entry.stack_frame_size = sfs;
173+
res.push(entry);
174+
}
175+
_ => (),
176+
}
177+
}
178+
}
179+
180+
fn estimate_stack_frame_size<R: gimli::Reader>(
181+
mut insns: gimli::CallFrameInstructionIter<'_, R>,
182+
) -> Result<u32, gimli::Error> {
183+
use gimli::CallFrameInstruction::*;
184+
185+
let mut sfs = 0;
186+
187+
loop {
188+
match insns.next() {
189+
Err(_e) => {
190+
break;
191+
}
192+
Ok(None) => {
193+
break;
194+
}
195+
Ok(Some(op)) => {
196+
if let DefCfaOffset { offset } = op {
197+
sfs = u32::max(sfs, offset as u32);
198+
}
199+
}
200+
}
201+
}
202+
203+
Ok(sfs)
204+
}

0 commit comments

Comments
 (0)