Repository: probe-rs/probe-rs
Pull request #3306 (2025-05-11 11:29:31)

Update the actually specified breakpoint unit
This PR fixes `CoreInterface::set_hw_breakpoint` to use the specified HW breakpoint unit, and adds a matching function to the inherent methods (Core::set_hw_breakpoint_unit). This means `CoreInterface::set_hw_breakpoint` can no longer fail with an unavailable breakpoint unit error if the specfied breakpoint unit exists.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index cd41d6da..a8f0fba8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3021,6 +3021,7 @@ dependencies = [
 name = "probe-rs-debug"
 version = "0.27.0"
 dependencies = [
+ "addr2line",
  "bitfield",
  "gimli 0.31.1",
  "insta",
diff --git a/changelog/added-core-set-hw-breakpoint-unit.md b/changelog/added-core-set-hw-breakpoint-unit.md
new file mode 100644
index 00000000..ae255205
--- /dev/null
+++ b/changelog/added-core-set-hw-breakpoint-unit.md
@@ -0,0 +1,1 @@
+Added `Core::set_hw_breakpoint_unit` to update a selected breakpoint unit.
\ No newline at end of file
diff --git a/changelog/changed-debugger-unknown.md b/changelog/changed-debugger-unknown.md
new file mode 100644
index 00000000..47475cf6
--- /dev/null
+++ b/changelog/changed-debugger-unknown.md
@@ -0,0 +1,1 @@
+The debugger now shows the name of the nearest symbol as the function name when debug info is not available.
\ No newline at end of file
diff --git a/probe-rs-debug/Cargo.toml b/probe-rs-debug/Cargo.toml
index aab32550..1bd3fd11 100644
--- a/probe-rs-debug/Cargo.toml
+++ b/probe-rs-debug/Cargo.toml
@@ -11,6 +11,7 @@ license.workspace = true
 description = "Debugging functionlity built on top of the probe-rs crate"
 
 [dependencies]
+addr2line = "0.24.2"
 bitfield = "0.19.0"
 gimli = "0.31.1"
 itertools = "0.14.0"
diff --git a/probe-rs-debug/src/debug_info.rs b/probe-rs-debug/src/debug_info.rs
index d7e29b1a..ca05ba98 100644
--- a/probe-rs-debug/src/debug_info.rs
+++ b/probe-rs-debug/src/debug_info.rs
@@ -6,9 +6,7 @@ use super::{
     unit_info::UnitInfo,
     variable::*,
 };
-use crate::{
-    SourceLocation, VerifiedBreakpoint, registers, stack_frame::StackFrameInfo, unit_info::RangeExt,
-};
+use crate::{SourceLocation, VerifiedBreakpoint, stack_frame::StackFrameInfo, unit_info::RangeExt};
 use gimli::{
     BaseAddresses, DebugFrame, RunTimeEndian, UnwindContext, UnwindSection, UnwindTableRow,
 };
@@ -38,14 +36,18 @@ pub struct DebugInfo {
 
     pub(crate) unit_infos: Vec<UnitInfo>,
     pub(crate) endianness: gimli::RunTimeEndian,
+
+    pub(crate) addr2line: Option<addr2line::Loader>,
 }
 
 impl DebugInfo {
     /// Read debug info directly from a ELF file.
     pub fn from_file<P: AsRef<Path>>(path: P) -> Result<DebugInfo, DebugError> {
-        let data = std::fs::read(path)?;
+        let data = std::fs::read(path.as_ref())?;
 
-        DebugInfo::from_raw(&data)
+        let mut this = DebugInfo::from_raw(&data)?;
+        this.addr2line = addr2line::Loader::new(path).ok();
+        Ok(this)
     }
 
     /// Parse debug information directly from a buffer containing an ELF file.
@@ -108,6 +110,7 @@ impl DebugInfo {
             debug_line_section,
             unit_infos,
             endianness,
+            addr2line: None,
         })
     }
 
@@ -312,6 +315,49 @@ impl DebugInfo {
         Ok(())
     }
 
+    /// Best-effort way to look up a function name without debuginfo.
+    fn get_stackframe_from_symbols(
+        &self,
+        address: u64,
+        unwind_registers: &DebugRegisters,
+    ) -> Result<Vec<StackFrame>, DebugError> {
+        let Some(ref addr2line) = self.addr2line else {
+            return Ok(vec![]);
+        };
+        let Some(fn_name) = addr2line.find_symbol(address) else {
+            return Ok(vec![]);
+        };
+
+        let mut fn_name = fn_name.to_string();
+        for lang in [
+            gimli::DW_LANG_Rust,
+            gimli::DW_LANG_C_plus_plus,
+            gimli::DW_LANG_C_plus_plus_03,
+            gimli::DW_LANG_C_plus_plus_11,
+            gimli::DW_LANG_C_plus_plus_14,
+        ] {
+            if let Some(demangle) = addr2line::demangle(&fn_name, lang) {
+                fn_name = demangle;
+                break;
+            }
+        }
+
+        Ok(vec![StackFrame {
+            id: get_object_reference(),
+            function_name: format!(
+                "{fn_name} @ {address:#0width$x}>",
+                width = (unwind_registers.get_address_size_bytes() * 2 + 2)
+            ),
+            source_location: None,
+            registers: unwind_registers.clone(),
+            pc: RegisterValue::from(address),
+            frame_base: None,
+            is_inlined: false,
+            local_variables: None,
+            canonical_frame_address: None,
+        }])
+    }
+
     /// Returns a populated (resolved) [`StackFrame`] struct.
     /// This function will also populate the `DebugInfo::VariableCache` with in scope `Variable`s for each `StackFrame`,
     /// while taking into account the appropriate strategy for lazy-loading of variables.
@@ -320,25 +366,24 @@ impl DebugInfo {
         memory: &mut impl MemoryInterface,
         address: u64,
         cfa: Option<u64>,
-        unwind_registers: &registers::DebugRegisters,
+        unwind_registers: &DebugRegisters,
     ) -> Result<Vec<StackFrame>, DebugError> {
         // When reporting the address, we format it as a hex string, with the width matching
         // the configured size of the datatype used in the `RegisterValue` address.
         let unknown_function = || {
             format!(
-                "<unknown function @ {:#0width$x}>",
-                address,
+                "<unknown function @ {address:#0width$x}>",
                 width = (unwind_registers.get_address_size_bytes() * 2 + 2)
             )
         };
 
         let Ok((unit_info, functions)) = self.get_function_dies(address) else {
             // No function found at the given address.
-            return Ok(vec![]);
+            return self.get_stackframe_from_symbols(address, unwind_registers);
         };
         if functions.is_empty() {
             // No function found at the given address.
-            return Ok(vec![]);
+            return self.get_stackframe_from_symbols(address, unwind_registers);
         }
 
         // The first function is the non-inlined function, and the rest are inlined functions.
@@ -503,7 +548,7 @@ impl DebugInfo {
 
     pub(crate) fn unwind_impl(
         &self,
-        initial_registers: registers::DebugRegisters,
+        initial_registers: DebugRegisters,
         memory: &mut impl MemoryInterface,
         exception_handler: &dyn ExceptionInterface,
         instruction_set: Option<InstructionSet>,
diff --git a/probe-rs/src/core.rs b/probe-rs/src/core.rs
index 7c4bd456..cbee6d4d 100644
--- a/probe-rs/src/core.rs
+++ b/probe-rs/src/core.rs
@@ -407,7 +407,7 @@ impl<'probe> Core<'probe> {
 
     /// Set a hardware breakpoint
     ///
-    /// This function will try to set a hardware breakpoint att `address`.
+    /// This function will try to set a hardware breakpoint at `address`.
     ///
     /// The amount of hardware breakpoints which are supported is chip specific,
     /// and can be queried using the `get_available_breakpoint_units` function.
@@ -436,8 +436,28 @@ impl<'probe> Core<'probe> {
 
         // Actually set the breakpoint. Even if it has been set, set it again so it will be active.
         self.inner
-            .set_hw_breakpoint(breakpoint_comparator_index, address)?;
-        Ok(())
+            .set_hw_breakpoint(breakpoint_comparator_index, address)
+    }
+
+    /// Set a hardware breakpoint
+    ///
+    /// This function will try to set a given hardware breakpoint unit to `address`.
+    ///
+    /// The amount of hardware breakpoints which are supported is chip specific,
+    /// and can be queried using the `get_available_breakpoint_units` function.
+    #[tracing::instrument(skip(self))]
+    pub fn set_hw_breakpoint_unit(&mut self, unit_index: usize, addr: u64) -> Result<(), Error> {
+        if !self.inner.hw_breakpoints_enabled() {
+            self.enable_breakpoints(true)?;
+        }
+
+        tracing::debug!(
+            "Trying to set HW breakpoint #{} with comparator address  {:#08x}",
+            unit_index,
+            addr
+        );
+
+        self.inner.set_hw_breakpoint(unit_index, addr)
     }
 
     /// Set a hardware breakpoint
@@ -600,8 +620,8 @@ impl CoreInterface for Core<'_> {
         self.enable_breakpoints(state)
     }
 
-    fn set_hw_breakpoint(&mut self, _unit_index: usize, addr: u64) -> Result<(), Error> {
-        self.set_hw_breakpoint(addr)
+    fn set_hw_breakpoint(&mut self, unit_index: usize, addr: u64) -> Result<(), Error> {
+        self.set_hw_breakpoint_unit(unit_index, addr)
     }
 
     fn clear_hw_breakpoint(&mut self, unit_index: usize) -> Result<(), Error> {


Test patch:
diff --git a/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__print_stacktrace.snap b/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__print_stacktrace.snap
index 9209c79e..795a35ef 100644
--- a/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__print_stacktrace.snap
+++ b/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__print_stacktrace.snap
@@ -66,7 +66,7 @@ Frame:
   column: Some(Column(1))
  frame_base:      Some(20003ff8)
 Frame:
- function:        <unknown function @ 0x000000ce> : ERROR: UNWIND: Failed to read value for register R7/FP from address 0x0000000000000000 (4 bytes): The coredump does not include the memory for address 0x0 of size 0x4
+ function:        Reset @ 0x000000ce> : ERROR: UNWIND: Failed to read value for register R7/FP from address 0x0000000000000000 (4 bytes): The coredump does not include the memory for address 0x0 of size 0x4
  source_location:
 None
  frame_base:      None
diff --git a/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_in_exception_handler.snap b/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_in_exception_handler.snap
index c84549ed..cd19898d 100644
--- a/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_in_exception_handler.snap
+++ b/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_in_exception_handler.snap
@@ -36,7 +36,7 @@ Frame:
   column: Some(Column(1))
  frame_base:      Some(2001fff8)
 Frame:
- function:        <unknown function @ 0x0000013c>
+ function:        Reset @ 0x0000013c>
  source_location:
 None
  frame_base:      None
diff --git a/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_in_exception_trampoline.snap b/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_in_exception_trampoline.snap
index a7db139a..1d5d92ae 100644
--- a/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_in_exception_trampoline.snap
+++ b/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_in_exception_trampoline.snap
@@ -29,7 +29,7 @@ Frame:
   column: Some(Column(1))
  frame_base:      Some(2001fff8)
 Frame:
- function:        <unknown function @ 0x0000013c>
+ function:        Reset @ 0x0000013c>
  source_location:
 None
  frame_base:      None
diff --git a/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_inlined.snap b/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_inlined.snap
index 65d0c676..14f8894a 100644
--- a/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_inlined.snap
+++ b/probe-rs-debug/src/snapshots/probe_rs_debug__debug_info__test__unwinding_inlined.snap
@@ -1,8 +1,6 @@
 ---
-source: probe-rs/src/debug/debug_info.rs
-assertion_line: 1896
+source: probe-rs-debug/src/debug_info.rs
 expression: printed_backtrace
-snapshot_kind: text
 ---
 Frame:
  function:        wait<nrf52833_pac::TIMER0, nrf_hal_common::timer::OneShot>
@@ -54,7 +52,7 @@ Frame:
   column: Some(Column(1))
  frame_base:      Some(20003ff8)
 Frame:
- function:        <unknown function @ 0x0000013c>
+ function:        Reset @ 0x0000013c>
  source_location:
 None
  frame_base:      None
diff --git a/smoke-tester/src/main.rs b/smoke-tester/src/main.rs
index 5fb78896..cc5b73af 100644
--- a/smoke-tester/src/main.rs
+++ b/smoke-tester/src/main.rs
@@ -478,6 +478,7 @@ impl<'dut> TestTracker<'dut> {
                 println_test_status!(self, yellow, "Missing resource for test: {}", message);
             }
             Err(_e) => {
+                println_test_status!(self, red, "{_e:?}");
                 println_test_status!(self, red, "Test failed in {formatted_duration}.");
             }
         };
diff --git a/smoke-tester/src/tests.rs b/smoke-tester/src/tests.rs
index d3ce7cf7..85a1a91e 100644
--- a/smoke-tester/src/tests.rs
+++ b/smoke-tester/src/tests.rs
@@ -247,17 +247,37 @@ fn test_hw_breakpoints(tracker: &TestTracker, core: &mut Core) -> TestResult {
 
         println_test_status!(tracker, blue, "{} breakpoints supported", num_breakpoints);
 
+        if num_breakpoints == 0 {
+            println_test_status!(tracker, blue, "No HW breakpoints supported");
+            continue;
+        }
+
+        let breakpoint_addresses = (0..num_breakpoints as u64)
+            .map(|i| initial_breakpoint_addr + 4 * i)
+            .collect::<Vec<_>>();
+
         // Test CoreInterface
-        for i in 0..num_breakpoints {
-            CoreInterface::set_hw_breakpoint(
-                core,
-                i as usize,
-                initial_breakpoint_addr + 4 * i as u64,
-            )
-            .into_diagnostic()?;
+        for (i, address) in breakpoint_addresses.iter().enumerate() {
+            CoreInterface::set_hw_breakpoint(core, i, *address).into_diagnostic()?;
+        }
+
+        let breakpoints = core.hw_breakpoints().into_diagnostic()?;
+        for (i, address) in breakpoint_addresses.iter().enumerate() {
+            assert_eq!(
+                breakpoints[i],
+                Some(*address),
+                "Error reading back HW breakpoint at index {i}"
+            );
         }
 
-        // TODO: check if a breakpoint can be overwritten - the position argument is currently ignored
+        // Now check that breakpoints can be overwritten.
+        CoreInterface::set_hw_breakpoint(core, 0, breakpoint_addresses[0] + 4).into_diagnostic()?;
+        let breakpoints = core.hw_breakpoints().into_diagnostic()?;
+        assert_eq!(
+            breakpoints[0],
+            Some(breakpoint_addresses[0] + 4),
+            "Error reading back HW breakpoint at index 0"
+        );
 
         // Clear all breakpoints again
         for i in 0..num_breakpoints {
@@ -274,8 +294,14 @@ fn test_hw_breakpoints(tracker: &TestTracker, core: &mut Core) -> TestResult {
         core.set_hw_breakpoint(initial_breakpoint_addr + num_breakpoints as u64 * 4)
             .expect_err("Trying to use more than supported number of breakpoints should fail.");
 
+        // However, we should be able to update a specific breakpoint
+        core.set_hw_breakpoint_unit(0, initial_breakpoint_addr + num_breakpoints as u64 * 4)
+            .into_diagnostic()?;
+
         // Clear all breakpoints again
-        for i in 0..num_breakpoints {
+        core.clear_hw_breakpoint(initial_breakpoint_addr + num_breakpoints as u64 * 4)
+            .into_diagnostic()?;
+        for i in 1..num_breakpoints {
             core.clear_hw_breakpoint(initial_breakpoint_addr + 4 * i as u64)
                 .into_diagnostic()?;
         }