Skip to main content

probe_rs_debug/
debug_step.rs

1use super::{DebugError, VerifiedBreakpoint, debug_info::DebugInfo};
2use probe_rs::{
3    CoreInterface, CoreStatus, Error, HaltReason,
4    architecture::{
5        arm::ArmError, riscv::communication_interface::RiscvError,
6        xtensa::communication_interface::XtensaError,
7    },
8};
9use std::{ops::RangeInclusive, time::Duration};
10
11/// Stepping granularity for stepping through a program during debug.
12#[derive(Clone, Debug)]
13pub enum SteppingMode {
14    /// Special case, where we aren't stepping, but we are trying to find the next valid breakpoint.
15    /// - The validity of halt locations are defined as target instructions that live between the end of the prologue, and the start of the end sequence of a [`gimli::read::LineRow`].
16    BreakPoint,
17    /// Advance one machine instruction at a time.
18    StepInstruction,
19    /// Step Over the current statement, and halt at the start of the next statement.
20    OverStatement,
21    /// Use best efforts to determine the location of any function calls in this statement, and step into them.
22    IntoStatement,
23    /// Step to the calling statement, immediately after the current function returns.
24    OutOfStatement,
25}
26
27impl SteppingMode {
28    /// Determine the program counter location where the SteppingMode is aimed, and step to it.
29    /// Return the new CoreStatus and program_counter value.
30    ///
31    /// Implementation Notes for stepping at statement granularity:
32    /// - If a hardware breakpoint is available, we will set it at the desired location, run to it, and release it.
33    /// - If no hardware breakpoints are available, we will do repeated instruction steps until we reach the desired location.
34    ///
35    /// Usage Note:
36    /// - Currently, no special provision is made for the effect of interrupts that get triggered
37    ///   during stepping. The user must ensure that interrupts are disabled during stepping, or
38    ///   accept that stepping may be diverted by the interrupt processing on the core.
39    pub fn step(
40        &self,
41        core: &mut impl CoreInterface,
42        debug_info: Option<&DebugInfo>,
43    ) -> Result<(CoreStatus, u64), DebugError> {
44        let mut core_status = core.status()?;
45        let mut program_counter = match core_status {
46            CoreStatus::Halted(_) => core
47                .read_core_reg(core.program_counter().id())?
48                .try_into()?,
49            _ => {
50                return Err(DebugError::Other(
51                    "Core must be halted before stepping.".to_string(),
52                ));
53            }
54        };
55        let origin_program_counter = program_counter;
56        let mut return_address = core.read_core_reg(core.return_address().id())?.try_into()?;
57
58        // Sometimes the target program_counter is at a location where the debug_info program row data does not contain valid statements for halt points.
59        // When DebugError::NoValidHaltLocation happens, we will step to the next instruction and try again(until we can reasonably expect to have passed out of an epilogue), before giving up.
60        let mut target_address: Option<u64> = None;
61        for _ in 0..10 {
62            let post_step_target = match self {
63                SteppingMode::StepInstruction => {
64                    // First deal with the the fast/easy case.
65                    program_counter = core.step()?.pc;
66                    core_status = core.status()?;
67                    return Ok((core_status, program_counter));
68                }
69                SteppingMode::BreakPoint => {
70                    let Some(debug_info) = debug_info else {
71                        return Err(DebugError::Other(
72                            "Cannot compute halt location without debug information".to_string(),
73                        ));
74                    };
75                    self.get_halt_location(core, debug_info, program_counter, None)
76                }
77                SteppingMode::IntoStatement
78                | SteppingMode::OverStatement
79                | SteppingMode::OutOfStatement => {
80                    let Some(debug_info) = debug_info else {
81                        return Err(DebugError::Other(
82                            "Cannot use statement stepping without debug information".to_string(),
83                        ));
84                    };
85                    // The more complex cases, where specific handling is required.
86                    self.get_halt_location(core, debug_info, program_counter, Some(return_address))
87                }
88            };
89            match post_step_target {
90                Ok(post_step_target) => {
91                    target_address = Some(post_step_target.address);
92                    // Re-read the program_counter, because it may have changed during the `get_halt_location` call.
93                    program_counter = core
94                        .read_core_reg(core.program_counter().id())?
95                        .try_into()?;
96                    break;
97                }
98                Err(error) => {
99                    match error {
100                        DebugError::WarnAndContinue { message } => {
101                            // Step on target instruction, and then try again.
102                            tracing::trace!(
103                                "Incomplete stepping information @{program_counter:#010X}: {message}"
104                            );
105                            program_counter = core.step()?.pc;
106                            return_address =
107                                core.read_core_reg(core.return_address().id())?.try_into()?;
108                            continue;
109                        }
110                        other_error => {
111                            core_status = core.status()?;
112                            program_counter = core
113                                .read_core_reg(core.program_counter().id())?
114                                .try_into()?;
115                            tracing::error!("Error during step ({:?}): {}", self, other_error);
116                            return Ok((core_status, program_counter));
117                        }
118                    }
119                }
120            }
121        }
122
123        (core_status, program_counter) = match target_address {
124            Some(target_address) => {
125                if let Some(debug_info) = debug_info {
126                    tracing::debug!(
127                        "Preparing to step ({:20?}): \n\tfrom: {:?} @ {:#010X} \n\t  to: {:?} @ {:#010X}",
128                        self,
129                        debug_info
130                            .get_source_location(program_counter)
131                            .map(|source_location| (
132                                source_location.path,
133                                source_location.line,
134                                source_location.column
135                            )),
136                        origin_program_counter,
137                        debug_info
138                            .get_source_location(target_address)
139                            .map(|source_location| (
140                                source_location.path,
141                                source_location.line,
142                                source_location.column
143                            )),
144                        target_address,
145                    );
146                } else {
147                    tracing::debug!(
148                        "Preparing to step ({:20?}): \n\tfrom: {:#010X} \n\t  to: {:#010X}",
149                        self,
150                        origin_program_counter,
151                        target_address,
152                    );
153                }
154
155                run_to_address(program_counter, target_address, core)?
156            }
157            None => {
158                return Err(DebugError::WarnAndContinue {
159                    message: "Unable to determine target address for this step request."
160                        .to_string(),
161                });
162            }
163        };
164        Ok((core_status, program_counter))
165    }
166
167    /// To understand how this method works, use the following framework:
168    /// - Everything is calculated from a given machine instruction address, usually the current program counter.
169    /// - To calculate where the user might step to (step-over, step-into, step-out), we start from the given instruction
170    ///   address/program counter, and work our way through all the rows in the sequence of instructions it is part of.
171    ///   - A sequence of instructions represents a series of monotonically increasing target machine instructions,
172    ///     and does not necessarily represent the whole of a function.
173    ///   - Similarly, the instructions belonging to a sequence are not necessarily contiguous inside the sequence of instructions,
174    ///     e.g. conditional branching inside the sequence.
175    /// - To determine valid halt points for breakpoints and stepping, we only use instructions that qualify as:
176    ///   - The beginning of a statement that is neither inside the prologue, nor inside the epilogue.
177    /// - Based on this, we will attempt to return the "most appropriate" address for the [`SteppingMode`], given the available information in the instruction sequence.
178    ///
179    /// All data is calculated using the [`gimli::read::CompleteLineProgram`] as well as, function call data from the debug info frame section.
180    ///
181    /// NOTE about errors returned: Sometimes the target program_counter is at a location where the debug_info program row data does not contain valid statements
182    /// for halt points, and we will return a `DebugError::NoValidHaltLocation`. In this case, we recommend the consumer of this API step the core to the next instruction
183    /// and try again, with a reasonable retry limit. All other error kinds are should be treated as non recoverable errors.
184    pub(crate) fn get_halt_location(
185        &self,
186        core: &mut impl CoreInterface,
187        debug_info: &DebugInfo,
188        program_counter: u64,
189        return_address: Option<u64>,
190    ) -> Result<VerifiedBreakpoint, DebugError> {
191        let program_unit = debug_info.compile_unit_info(program_counter)?;
192        match self {
193            SteppingMode::BreakPoint => {
194                // Find the first_breakpoint_address
195                return VerifiedBreakpoint::for_address(debug_info, program_counter);
196            }
197            SteppingMode::OverStatement => {
198                // Find the "step over location"
199                // - The instructions in a sequence do not necessarily have contiguous addresses,
200                //   and the next instruction address may be affected by conditional branching at runtime.
201                // - Therefore, in order to find the correct "step over location", we iterate through the
202                //   instructions to find the starting address of the next halt location, ie. the address
203                //   is greater than the current program counter.
204                //    -- If there is one, it means the step over target is in the current sequence,
205                //       so we get the valid breakpoint location for this next location.
206                //    -- If there is not one, the step over target is the same as the step out target.
207                return VerifiedBreakpoint::for_address(
208                    debug_info,
209                    program_counter.saturating_add(1),
210                )
211                .or_else(|_| {
212                    // If we cannot find a valid breakpoint in the current sequence, we will step out of the current sequence.
213                    SteppingMode::OutOfStatement.get_halt_location(
214                        core,
215                        debug_info,
216                        program_counter,
217                        return_address,
218                    )
219                });
220            }
221            SteppingMode::IntoStatement => {
222                // This is a tricky case because the current RUST generated DWARF, does not store the DW_TAG_call_site information described in the DWARF 5 standard.
223                // - It is not a mandatory attribute, so not sure if we can ever expect it.
224                // To find if any functions are called from the current program counter:
225                // 1. Identify the next instruction location after the instruction corresponding to the current PC,
226                // 2. Single step the target core, until either of the following:
227                //   (a) We hit a PC that is NOT in the range between the current PC and the next instruction location.
228                //       This location, which could be any of the following:
229                //          (a.i)  A legitimate branch outside the current sequence (call to another instruction) such as
230                //                 an explicit call to a function, or something the compiler injected, like a `drop()`,
231                //          (a.ii) An interrupt handler diverted the processing.
232                //   (b) We hit a PC at the address of the identified next instruction location,
233                //       which means there was nothing to step into, so the target is now halted (correctly) at the next statement.
234                let target_pc = match VerifiedBreakpoint::for_address(
235                    debug_info,
236                    program_counter.saturating_add(1),
237                ) {
238                    Ok(identified_next_breakpoint) => identified_next_breakpoint.address,
239                    Err(DebugError::WarnAndContinue { .. }) => {
240                        // There are no next statements in this sequence, so we will use the return address as the target.
241                        if let Some(return_address) = return_address {
242                            return_address
243                        } else {
244                            return Err(DebugError::WarnAndContinue {
245                                message: "Could not determine a 'step in' target. Please use 'step over'.".to_string(),
246                            });
247                        }
248                    }
249                    Err(other_error) => {
250                        return Err(other_error);
251                    }
252                };
253
254                let (core_status, new_pc) = step_to_address(program_counter..=target_pc, core)?;
255                if (program_counter..=target_pc).contains(&new_pc) {
256                    // We have halted at an address after the current instruction (either in the same sequence,
257                    // or at the return address of the current function),
258                    // so we can conclude there were no branching calls in this instruction.
259                    tracing::debug!(
260                        "Stepping into next statement, but no branching calls found. Stepped to next available location."
261                    );
262                } else if matches!(core_status, CoreStatus::Halted(HaltReason::Breakpoint(_))) {
263                    // We have halted at a PC that is within the current statement, so there must be another breakpoint.
264                    tracing::debug!("Stepping into next statement, but encountered a breakpoint.");
265                } else {
266                    tracing::debug!("Stepping into next statement at address: {:#010x}.", new_pc);
267                }
268
269                return SteppingMode::BreakPoint.get_halt_location(core, debug_info, new_pc, None);
270            }
271            SteppingMode::OutOfStatement => {
272                if let Ok(function_dies) =
273                    program_unit.get_function_dies(debug_info, program_counter)
274                {
275                    // We want the first qualifying (PC is in range) function from the back of this list,
276                    // to access the 'innermost' functions first.
277                    if let Some(function) = function_dies.iter().next_back() {
278                        tracing::trace!(
279                            "Step Out target: Evaluating function {:?}, low_pc={:?}, high_pc={:?}",
280                            function.function_name(debug_info),
281                            function.low_pc(),
282                            function.high_pc()
283                        );
284
285                        if function
286                            .attribute(debug_info, gimli::DW_AT_noreturn)
287                            .is_some()
288                        {
289                            return Err(DebugError::Other(format!(
290                                "Function {:?} is marked as `noreturn`. Cannot step out of this function.",
291                                function
292                                    .function_name(debug_info)
293                                    .as_deref()
294                                    .unwrap_or("<unknown>")
295                            )));
296                        } else if function.range_contains(program_counter) {
297                            if function.is_inline() {
298                                // Step_out_address for inlined functions, is the first available breakpoint address after the last statement in the inline function.
299                                let (_, next_instruction_address) = run_to_address(
300                                    program_counter,
301                                    function.high_pc().unwrap(), //unwrap is OK because `range_contains` is true.
302                                    core,
303                                )?;
304                                return SteppingMode::BreakPoint.get_halt_location(
305                                    core,
306                                    debug_info,
307                                    next_instruction_address,
308                                    None,
309                                );
310                            } else if let Some(return_address) = return_address {
311                                tracing::debug!(
312                                    "Step Out target: non-inline function, stepping over return address: {:#010x}",
313                                    return_address
314                                );
315                                // Step_out_address for non-inlined functions is the first available breakpoint address after the return address.
316                                return SteppingMode::BreakPoint.get_halt_location(
317                                    core,
318                                    debug_info,
319                                    return_address,
320                                    None,
321                                );
322                            }
323                        }
324                    }
325                }
326            }
327            _ => {
328                // SteppingMode::StepInstruction is handled in the `step()` method.
329            }
330        }
331
332        Err(DebugError::WarnAndContinue {
333                message: "Could not determine valid halt locations for this request. Please consider using instruction level stepping.".to_string()
334        })
335    }
336}
337
338/// Run the target to the desired address. If available, we will use a breakpoint, otherwise we will use single step.
339/// Returns the program counter at the end of the step, when any of the following conditions are met:
340/// - We reach the `target_address_range.end()` (inclusive)
341/// - We reach some other legitimate halt point (e.g. the user tries to step past a series of statements, but there is another breakpoint active in that "gap")
342/// - We encounter an error (e.g. the core locks up, or the USB cable is unplugged, etc.)
343/// - It turns out this step will be long-running, and we do not have to wait any longer for the request to complete.
344fn run_to_address(
345    mut program_counter: u64,
346    target_address: u64,
347    core: &mut impl CoreInterface,
348) -> Result<(CoreStatus, u64), DebugError> {
349    if target_address == program_counter {
350        // No need to step further. e.g. For inline functions we have already stepped to the best available target address..
351        return Ok((
352            core.status()?,
353            core.read_core_reg(core.program_counter().id())?
354                .try_into()?,
355        ));
356    }
357
358    let breakpoints = core.hw_breakpoints()?;
359    let bp_to_use = breakpoints.iter().position(|bp| bp.is_none()).unwrap_or(0);
360
361    if core.set_hw_breakpoint(bp_to_use, target_address).is_ok() {
362        core.run()?;
363        // It is possible that we are stepping over long running instructions.
364        let status = core.wait_for_core_halted(Duration::from_millis(1000));
365
366        // Restore the original breakpoint.
367        if let Some(Some(bp)) = breakpoints.get(bp_to_use) {
368            core.set_hw_breakpoint(bp_to_use, *bp)?;
369        } else {
370            core.clear_hw_breakpoint(bp_to_use)?;
371        }
372
373        match status {
374            Ok(()) => {
375                // We have hit the target address, so all is good.
376                // NOTE: It is conceivable that the core has halted, but we have not yet stepped to the target address. (e.g. the user tries to step out of a function, but there is another breakpoint active before the end of the function.)
377                //       This is a legitimate situation, so we clear the breakpoint at the target address, and pass control back to the user
378                Ok((
379                    core.status()?,
380                    core.read_core_reg(core.program_counter().id())?
381                        .try_into()?,
382                ))
383            }
384            Err(error) => {
385                program_counter = core.halt(Duration::from_millis(500))?.pc;
386                if matches!(
387                    error,
388                    Error::Arm(ArmError::Timeout)
389                        | Error::Riscv(RiscvError::Timeout)
390                        | Error::Xtensa(XtensaError::Timeout)
391                ) {
392                    // This is not a quick step and halt operation. Notify the user that we are not going to wait any longer, and then return the current program counter so that the debugger can show the user where the forced halt happened.
393                    tracing::error!(
394                        "The core did not halt after stepping to {:#010X}. Forced a halt at {:#010X}. Long running operations between debug steps are not currently supported.",
395                        target_address,
396                        program_counter
397                    );
398                    Ok((core.status()?, program_counter))
399                } else {
400                    // Something else is wrong.
401                    Err(DebugError::Other(format!(
402                        "Unexpected error while waiting for the core to halt after stepping to {program_counter:#010X}. Forced a halt at {target_address:#010X}. {error:?}."
403                    )))
404                }
405            }
406        }
407    } else {
408        // If we don't have breakpoints to use, we have to rely on single stepping.
409        // TODO: In theory, this could go on for a long time. Should we consider NOT allowing this kind of stepping if there are no breakpoints available?
410
411        Ok(step_to_address(target_address..=u64::MAX, core)?)
412    }
413}
414
415/// In some cases, we need to single-step the core, until ONE of the following conditions are met:
416/// - We reach the `target_address_range.end()`
417/// - We reach an address that is not in the sequential range of `target_address_range`,
418///   i.e. we stepped to some kind of branch instruction, or diversion to an interrupt handler.
419/// - We reach some other legitimate halt point (e.g. the user tries to step past a series of statements,
420///   but there is another breakpoint active in that "gap")
421/// - We encounter an error (e.g. the core locks up).
422fn step_to_address(
423    target_address_range: RangeInclusive<u64>,
424    core: &mut impl CoreInterface,
425) -> Result<(CoreStatus, u64), DebugError> {
426    while target_address_range.contains(&core.step()?.pc) {
427        // Single step the core until we get to the target_address;
428        match core.status()? {
429            CoreStatus::Halted(halt_reason) => match halt_reason {
430                HaltReason::Step | HaltReason::Request => continue,
431                HaltReason::Breakpoint(_) => {
432                    tracing::debug!(
433                        "Encountered a breakpoint before the target address ({:#010x}) was reached.",
434                        target_address_range.end()
435                    );
436                    break;
437                }
438                // This is a recoverable error kind, and can be reported to the user higher up in the call stack.
439                other_halt_reason => {
440                    return Err(DebugError::WarnAndContinue {
441                        message: format!(
442                            "Target halted unexpectedly before we reached the destination address of a step operation: {other_halt_reason:?}"
443                        ),
444                    });
445                }
446            },
447            // This is not a recoverable error, and will result in the debug session ending (we have no predictable way of successfully continuing the session)
448            other_status => {
449                return Err(DebugError::Other(format!(
450                    "Target failed to reach the destination address of a step operation: {other_status:?}"
451                )));
452            }
453        }
454    }
455    Ok((
456        core.status()?,
457        core.read_core_reg(core.program_counter().id())?
458            .try_into()?,
459    ))
460}