use anyhow::{anyhow, Context as _};
use gimli::{
BaseAddresses, CieOrFde, DebugFrame, FrameDescriptionEntry, Reader, UnwindContext,
UnwindSection as _,
};
use probe_rs::{config::RamRegion, Core};
use crate::{
backtrace::Outcome,
cortexm,
elf::Elf,
registers::{self, Registers},
stacked::Stacked,
target_info::TargetInfo,
};
fn missing_debug_info(pc: u32) -> String {
format!("debug information for address {pc:#x} is missing. Likely fixes:
1. compile the Rust code with `debug = 1` or higher. This is configured in the `profile.{{release,bench}}` sections of Cargo.toml (`profile.{{dev,test}}` default to `debug = 2`)
2. use a recent version of the `cortex-m` crates (e.g. cortex-m 0.6.3 or newer). Check versions in Cargo.lock
3. if linking to C code, compile the C code with the `-g` flag")
}
pub fn target(core: &mut Core, elf: &Elf, target_info: &TargetInfo) -> Output {
let mut output = Output {
corrupted: true,
outcome: Outcome::Ok,
raw_frames: vec![],
processing_error: None,
};
macro_rules! unwrap_or_return_output {
( $e:expr ) => {
match $e {
Ok(x) => x,
Err(err) => {
output.processing_error = Some(anyhow!(err));
return output;
}
}
};
}
let mut pc = unwrap_or_return_output!(core.read_core_reg(registers::PC));
let sp = unwrap_or_return_output!(core.read_core_reg(registers::SP));
let lr = unwrap_or_return_output!(core.read_core_reg(registers::LR));
let base_addresses = BaseAddresses::default();
let mut unwind_context = UnwindContext::new();
let mut registers = Registers::new(lr, sp, core);
let active_ram_region = &target_info.active_ram_region;
loop {
if let Some(outcome) =
check_hard_fault(pc, &elf.vector_table, &mut output, sp, active_ram_region)
{
output.outcome = outcome;
}
output.raw_frames.push(RawFrame::Subroutine { pc });
let fde = unwrap_or_return_output!(find_fde(&elf.debug_frame, &base_addresses, pc));
let uwt_row = unwrap_or_return_output!(fde
.unwind_info_for_address(
&elf.debug_frame,
&base_addresses,
&mut unwind_context,
pc.into()
)
.with_context(|| missing_debug_info(pc)));
log::trace!("uwt row for pc {pc:#010x}: {uwt_row:?}");
let cfa_changed = unwrap_or_return_output!(registers.update_cfa(uwt_row.cfa()));
for (reg, rule) in uwt_row.registers() {
unwrap_or_return_output!(registers.update(reg, rule));
}
let lr = unwrap_or_return_output!(registers.get(registers::LR));
log::debug!("LR={lr:#010X} PC={pc:#010X}");
let exception_entry = lr >= cortexm::EXC_RETURN_MARKER;
let program_counter_changed = !cortexm::subroutine_eq(lr, pc);
if !cfa_changed && !program_counter_changed {
let reset_fn_range = elf.reset_fn_range();
output.corrupted = !(reset_fn_range.contains(&pc) || reset_fn_range.is_empty());
break;
}
if exception_entry {
output.raw_frames.push(RawFrame::Exception);
let fpu = lr & cortexm::EXC_RETURN_FTYPE_MASK == 0;
let sp = unwrap_or_return_output!(registers.get(registers::SP));
let ram_bounds = active_ram_region
.as_ref()
.map(|ram_region| {
ram_region.range.start.try_into().unwrap_or(u32::MAX)
..ram_region.range.end.try_into().unwrap_or(u32::MAX)
})
.unwrap_or(cortexm::VALID_RAM_ADDRESS);
let stacked = if let Some(stacked) =
unwrap_or_return_output!(Stacked::read(registers.core, sp, fpu, ram_bounds))
{
stacked
} else {
output.corrupted = true;
break;
};
registers.insert(registers::LR, stacked.lr);
registers.insert(registers::SP, sp + stacked.size());
pc = stacked.pc;
} else if cortexm::is_thumb_bit_set(lr) {
pc = cortexm::clear_thumb_bit(lr);
} else {
output.processing_error = Some(anyhow!(
"bug? LR ({lr:#010x}) didn't have the Thumb bit set",
));
break;
}
match registers.get(registers::SP) {
Ok(advanced_sp) if advanced_sp > target_info.stack_start => {
output.corrupted = true;
break;
}
_ => {}
}
}
output
}
fn check_hard_fault(
pc: u32,
vector_table: &cortexm::VectorTable,
output: &mut Output,
sp: u32,
sp_ram_region: &Option<RamRegion>,
) -> Option<Outcome> {
if cortexm::is_hard_fault(pc, vector_table) {
assert!(
output.raw_frames.is_empty(),
"when present HardFault handler must be the first frame we unwind but wasn't"
);
if overflowed_stack(sp, sp_ram_region) {
return Some(Outcome::StackOverflow);
} else {
return Some(Outcome::HardFault);
}
}
None
}
#[derive(Debug)]
pub struct Output {
pub corrupted: bool,
pub outcome: Outcome,
pub raw_frames: Vec<RawFrame>,
pub processing_error: Option<anyhow::Error>,
}
#[derive(Debug)]
pub enum RawFrame {
Subroutine { pc: u32 },
Exception,
}
impl RawFrame {
pub fn is_exception(&self) -> bool {
matches!(self, Self::Exception)
}
}
fn overflowed_stack(sp: u32, active_ram_region: &Option<RamRegion>) -> bool {
if let Some(active_ram_region) = active_ram_region {
let range = active_ram_region.range.start..=active_ram_region.range.end;
!range.contains(&sp.into())
} else {
log::warn!("no RAM region appears to contain the stack; probe-run can't determine if this was a stack overflow");
false
}
}
fn find_fde<R: Reader>(
debug_frame: &DebugFrame<R>,
bases: &BaseAddresses,
addr: u32,
) -> anyhow::Result<FrameDescriptionEntry<R>> {
let mut entries = debug_frame.entries(bases);
let mut fdes = Vec::new();
while let Some(entry) = entries.next()? {
match entry {
CieOrFde::Cie(_) => {}
CieOrFde::Fde(partial) => {
let fde = partial.parse(DebugFrame::cie_from_offset)?;
if fde.initial_address() == 0 {
continue;
}
if fde.contains(addr.into()) {
log::trace!(
"{addr:#010x}: found FDE for {:#010x} .. {:#010x} at offset {:?}",
fde.initial_address(),
fde.initial_address() + fde.len(),
fde.offset(),
);
fdes.push(fde);
}
}
}
}
match fdes.len() {
0 => Err(anyhow!(gimli::Error::NoUnwindInfoForAddress))
.with_context(|| missing_debug_info(addr)),
1 => Ok(fdes.pop().unwrap()),
n => Err(anyhow!(
"found {n} frame description entries for address {addr:#010x}, there should only be 1; \
this is likely a bug in your compiler toolchain; unwinding will stop here",
)),
}
}