use probe_rs::InstructionSet;
use probe_rs::MemoryInterface;
use probe_rs::RegisterValue;
use probe_rs_debug::unwind_program_counter_register;
use super::FunctionAddress;
#[derive(Debug, thiserror::Error)]
pub(crate) enum FramePointerStackWalkError {
#[error("Could not determine instruction set")]
DetermineInstructionSet(#[source] probe_rs::Error),
#[error("Could not read register")]
ReadAddressOverflow,
#[error("Could not read register")]
ReadRegister(#[source] probe_rs::Error),
#[error("Could not read frame record memory")]
ReadMemory(#[source] probe_rs::Error),
#[error("Failed to spill registers")]
RegisterSpillError(#[source] probe_rs::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FrameRecord32 {
frame_pointer: u32,
return_address: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FrameRecord64 {
frame_pointer: u64,
return_address: u64,
}
fn read_arm_riscv_32_frame_record(
memory: &mut impl MemoryInterface,
frame_pointer: u64,
frame_record_offset: i64,
) -> Result<FrameRecord32, FramePointerStackWalkError> {
let read_addr = frame_pointer
.checked_add_signed(frame_record_offset)
.ok_or(FramePointerStackWalkError::ReadAddressOverflow)?;
let mut frame_record = [0; 2];
memory
.read_32(read_addr, &mut frame_record)
.map_err(FramePointerStackWalkError::ReadMemory)?;
let [caller_fp, return_address] = frame_record;
Ok(FrameRecord32 {
frame_pointer: caller_fp,
return_address,
})
}
fn read_arm_riscv_64_frame_record(
memory: &mut impl MemoryInterface,
frame_pointer: u64,
frame_record_offset: i64,
) -> Result<FrameRecord64, FramePointerStackWalkError> {
let read_addr = frame_pointer
.checked_add_signed(frame_record_offset)
.ok_or(FramePointerStackWalkError::ReadAddressOverflow)?;
let mut frame_record = [0; 2];
memory
.read_64(read_addr, &mut frame_record)
.map_err(FramePointerStackWalkError::ReadMemory)?;
let [caller_fp, return_address] = frame_record;
Ok(FrameRecord64 {
frame_pointer: caller_fp,
return_address,
})
}
fn read_xtensa_frame_record(
memory: &mut impl MemoryInterface,
frame_pointer: u64,
frame_record_offset: i64,
) -> Result<FrameRecord32, FramePointerStackWalkError> {
let read_addr = frame_pointer
.checked_add_signed(frame_record_offset)
.ok_or(FramePointerStackWalkError::ReadAddressOverflow)?;
let mut frame_record = [0; 2];
memory
.read_32(read_addr, &mut frame_record)
.map_err(FramePointerStackWalkError::ReadMemory)?;
let [return_address, caller_fp] = frame_record;
Ok(FrameRecord32 {
frame_pointer: caller_fp,
return_address,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct AdjustedFrameRecord {
frame_pointer: u64,
adjusted_return_address: u64,
}
impl AdjustedFrameRecord {
fn new_from_frame_record_32(
fr_32: FrameRecord32,
instruction_set: InstructionSet,
last_pc: u64,
) -> Self {
let ra = RegisterValue::U32(fr_32.return_address);
let adjusted_return_address = if ra.is_zero() || ra.is_max_value() {
ra
} else {
unwind_program_counter_register(ra, last_pc, Some(instruction_set))
.expect("Valid return address unwound")
};
Self {
frame_pointer: fr_32.frame_pointer as u64,
adjusted_return_address: adjusted_return_address
.try_into()
.expect("Should be able to convert 32-bit return address to u64"),
}
}
fn new_from_frame_record_64(
fr_64: FrameRecord64,
instruction_set: InstructionSet,
last_pc: u64,
) -> Self {
let ra = RegisterValue::U64(fr_64.return_address);
let adjusted_return_address = if ra.is_zero() || ra.is_max_value() {
ra
} else {
unwind_program_counter_register(ra, last_pc, Some(instruction_set))
.expect("Valid return address unwound")
};
Self {
frame_pointer: fr_64.frame_pointer,
adjusted_return_address: adjusted_return_address
.try_into()
.expect("Should be able to convert 64-bit return address to u64"),
}
}
}
fn read_frame_record_for_core(
memory: &mut impl MemoryInterface,
instruction_set: InstructionSet,
frame_pointer: u64,
last_pc: u64,
) -> Result<AdjustedFrameRecord, FramePointerStackWalkError> {
const ARM32_FRAME_RECORD_OFFSET: i64 = 0;
const ARM64_FRAME_RECORD_OFFSET: i64 = 0;
const RISCV32_FRAME_RECORD_OFFSET: i64 = -8;
const RISCV64_FRAME_RECORD_OFFSET: i64 = -16;
const XTENSA_FRAME_RECORD_OFFSET: i64 = -16;
match instruction_set {
InstructionSet::A32 | InstructionSet::Thumb2 => {
read_arm_riscv_32_frame_record(memory, frame_pointer, ARM32_FRAME_RECORD_OFFSET).map(
|fr| AdjustedFrameRecord::new_from_frame_record_32(fr, instruction_set, last_pc),
)
}
InstructionSet::A64 => {
read_arm_riscv_64_frame_record(memory, frame_pointer, ARM64_FRAME_RECORD_OFFSET).map(
|fr| AdjustedFrameRecord::new_from_frame_record_64(fr, instruction_set, last_pc),
)
}
InstructionSet::RV32 | InstructionSet::RV32C => {
read_arm_riscv_32_frame_record(memory, frame_pointer, RISCV32_FRAME_RECORD_OFFSET).map(
|fr| AdjustedFrameRecord::new_from_frame_record_32(fr, instruction_set, last_pc),
)
}
InstructionSet::RV64 | InstructionSet::RV64C => {
read_arm_riscv_64_frame_record(memory, frame_pointer, RISCV64_FRAME_RECORD_OFFSET).map(
|fr| AdjustedFrameRecord::new_from_frame_record_64(fr, instruction_set, last_pc),
)
}
InstructionSet::Xtensa => {
read_xtensa_frame_record(memory, frame_pointer, XTENSA_FRAME_RECORD_OFFSET).map(|fr| {
AdjustedFrameRecord::new_from_frame_record_32(fr, instruction_set, last_pc)
})
}
}
}
fn frame_pointer_stack_walk_memory_interface(
memory: &mut impl MemoryInterface,
instruction_set: InstructionSet,
entry_point_address_range: &std::ops::Range<u64>,
program_counter: u64,
mut frame_pointer: u64,
) -> Result<Vec<FunctionAddress>, FramePointerStackWalkError> {
let mut stack_frames = Vec::new();
stack_frames.push(FunctionAddress::ProgramCounter(program_counter));
let mut last_frame_pointer = frame_pointer;
let mut last_program_counter = program_counter;
while frame_pointer != 0 {
let adjusted_return_address;
AdjustedFrameRecord {
frame_pointer,
adjusted_return_address,
} = read_frame_record_for_core(
memory,
instruction_set,
frame_pointer,
last_program_counter,
)?;
if last_frame_pointer >= frame_pointer {
break;
}
last_frame_pointer = frame_pointer;
stack_frames.push(FunctionAddress::AdjustedReturnAddress(
adjusted_return_address,
));
if entry_point_address_range.contains(&adjusted_return_address) {
break;
}
last_program_counter = adjusted_return_address;
}
Ok(stack_frames.into_iter().rev().collect())
}
pub(crate) fn frame_pointer_stack_walk<'a>(
core: &mut probe_rs::Core<'a>,
entry_point_address_range: &std::ops::Range<u64>,
) -> Result<Vec<FunctionAddress>, FramePointerStackWalkError> {
let instruction_set = core
.instruction_set()
.map_err(FramePointerStackWalkError::DetermineInstructionSet)?;
if instruction_set == InstructionSet::Xtensa {
core.spill_registers()
.map_err(FramePointerStackWalkError::RegisterSpillError)?;
}
let fp_reg = match instruction_set {
InstructionSet::Xtensa => core.stack_pointer(),
_ => core.frame_pointer(),
};
let frame_pointer: u64 = core
.read_core_reg(fp_reg)
.map_err(FramePointerStackWalkError::ReadRegister)?;
let program_counter: u64 = core
.read_core_reg(core.program_counter())
.map_err(FramePointerStackWalkError::ReadRegister)?;
frame_pointer_stack_walk_memory_interface(
core,
instruction_set,
entry_point_address_range,
program_counter,
frame_pointer,
)
}
#[cfg(test)]
mod test {
use probe_rs_debug::DebugRegisters;
use probe_rs::{CoreDump, RegisterRole};
use super::super::test::{addresses_to_callstack, coredump_path, get_path_for_test_files};
use super::*;
fn frame_pointer_stack_walk_core_dump(
core_dump: &mut CoreDump,
entry_point_address_range: &std::ops::Range<u64>,
) -> Result<Vec<FunctionAddress>, FramePointerStackWalkError> {
let instruction_set = core_dump.instruction_set();
let fp_role = match instruction_set {
InstructionSet::Xtensa => RegisterRole::StackPointer,
_ => RegisterRole::FramePointer,
};
let initial_registers = DebugRegisters::from_coredump(core_dump);
let frame_pointer = initial_registers
.get_register_value_by_role(&fp_role)
.unwrap();
let program_counter = initial_registers
.get_register_value_by_role(&RegisterRole::ProgramCounter)
.unwrap();
frame_pointer_stack_walk_memory_interface(
core_dump,
instruction_set,
entry_point_address_range,
program_counter,
frame_pointer,
)
}
fn check_stack_walk(test_name: &str, expect: &Vec<FunctionAddress>) {
let executable_location = get_path_for_test_files(format!("{test_name}.elf").as_str());
let coredump_path = coredump_path(test_name);
let mut core_dump = CoreDump::load(&coredump_path).unwrap();
let object_bytes = std::fs::read(&executable_location).unwrap();
let obj = object::File::parse(object_bytes.as_slice()).unwrap();
let entry_point_address_range = super::super::get_entry_point_address_range(&obj).unwrap();
let res =
frame_pointer_stack_walk_core_dump(&mut core_dump, &entry_point_address_range).unwrap();
assert_eq!(&res, expect);
}
#[test]
fn test_frame_pointer_stack_walk_riscv32() {
let test_name = "esp32c6_coredump_elf";
let expect = addresses_to_callstack(&[
0x4200124e, 0x420054f2, 0x42000202, 0x420052ec, 0x42000244, 0x42000510, 0x4200438c, 0x42000132, ]);
check_stack_walk(test_name, &expect);
}
#[test]
fn test_frame_pointer_stack_walk_armv7em() {
let test_name = "atsamd51p19a";
let expect = addresses_to_callstack(&[
0x1474, 0x14da, 0x1538, 0x978, ]);
check_stack_walk(test_name, &expect);
}
#[test]
fn test_frame_pointer_stack_walk_xtensa() {
let test_name = "esp32s3_coredump_elf";
let expect = addresses_to_callstack(&[
0x420045e3, 0x42000f69, 0x42000d10, 0x42004c4e, 0x42000bb9, 0x42000f7f, 0x42004483, 0x40378836, ]);
check_stack_walk(test_name, &expect);
}
}