probe_rs/debug/
debug_step.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
use super::{debug_info::DebugInfo, DebugError, VerifiedBreakpoint};
use crate::{
    architecture::{
        arm::ArmError, riscv::communication_interface::RiscvError,
        xtensa::communication_interface::XtensaError,
    },
    CoreInterface, CoreStatus, HaltReason,
};
use std::{ops::RangeInclusive, time::Duration};

/// Stepping granularity for stepping through a program during debug.
#[derive(Clone, Debug)]
pub enum SteppingMode {
    /// Special case, where we aren't stepping, but we are trying to find the next valid breakpoint.
    /// - 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`].
    BreakPoint,
    /// Advance one machine instruction at a time.
    StepInstruction,
    /// Step Over the current statement, and halt at the start of the next statement.
    OverStatement,
    /// Use best efforts to determine the location of any function calls in this statement, and step into them.
    IntoStatement,
    /// Step to the calling statement, immediately after the current function returns.
    OutOfStatement,
}

impl SteppingMode {
    /// Determine the program counter location where the SteppingMode is aimed, and step to it.
    /// Return the new CoreStatus and program_counter value.
    ///
    /// Implementation Notes for stepping at statement granularity:
    /// - If a hardware breakpoint is available, we will set it at the desired location, run to it, and release it.
    /// - If no hardware breakpoints are available, we will do repeated instruction steps until we reach the desired location.
    ///
    /// Usage Note:
    /// - Currently, no special provision is made for the effect of interrupts that get triggered
    ///   during stepping. The user must ensure that interrupts are disabled during stepping, or
    ///   accept that stepping may be diverted by the interrupt processing on the core.
    pub fn step(
        &self,
        core: &mut impl CoreInterface,
        debug_info: &DebugInfo,
    ) -> Result<(CoreStatus, u64), DebugError> {
        let mut core_status = core.status()?;
        let mut program_counter = match core_status {
            CoreStatus::Halted(_) => core
                .read_core_reg(core.program_counter().id())?
                .try_into()?,
            _ => {
                return Err(DebugError::Other(
                    "Core must be halted before stepping.".to_string(),
                ))
            }
        };
        let origin_program_counter = program_counter;
        let mut return_address = core.read_core_reg(core.return_address().id())?.try_into()?;

        // Sometimes the target program_counter is at a location where the debug_info program row data does not contain valid statements for halt points.
        // 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.
        let mut target_address: Option<u64> = None;
        for _ in 0..10 {
            let post_step_target = match self {
                SteppingMode::StepInstruction => {
                    // First deal with the the fast/easy case.
                    program_counter = core.step()?.pc;
                    core_status = core.status()?;
                    return Ok((core_status, program_counter));
                }
                SteppingMode::BreakPoint => {
                    self.get_halt_location(core, debug_info, program_counter, None)
                }
                SteppingMode::IntoStatement
                | SteppingMode::OverStatement
                | SteppingMode::OutOfStatement => {
                    // The more complex cases, where specific handling is required.
                    self.get_halt_location(core, debug_info, program_counter, Some(return_address))
                }
            };
            match post_step_target {
                Ok(post_step_target) => {
                    target_address = Some(post_step_target.address);
                    // Re-read the program_counter, because it may have changed during the `get_halt_location` call.
                    program_counter = core
                        .read_core_reg(core.program_counter().id())?
                        .try_into()?;
                    break;
                }
                Err(error) => {
                    match error {
                        DebugError::WarnAndContinue { message } => {
                            // Step on target instruction, and then try again.
                            tracing::trace!("Incomplete stepping information @{program_counter:#010X}: {message}");
                            program_counter = core.step()?.pc;
                            return_address =
                                core.read_core_reg(core.return_address().id())?.try_into()?;
                            continue;
                        }
                        other_error => {
                            core_status = core.status()?;
                            program_counter = core
                                .read_core_reg(core.program_counter().id())?
                                .try_into()?;
                            tracing::error!("Error during step ({:?}): {}", self, other_error);
                            return Ok((core_status, program_counter));
                        }
                    }
                }
            }
        }

        (core_status, program_counter) = match target_address {
            Some(target_address) => {
                tracing::debug!(
                    "Preparing to step ({:20?}): \n\tfrom: {:?} @ {:#010X} \n\t  to: {:?} @ {:#010X}",
                    self,
                    debug_info
                        .get_source_location(program_counter)
                        .map(|source_location| (
                            source_location.path,
                            source_location.line,
                            source_location.column
                        )),
                    origin_program_counter,
                    debug_info
                        .get_source_location(target_address)
                        .map(|source_location| (
                            source_location.path,
                            source_location.line,
                            source_location.column
                        )),
                    target_address,
                );

                run_to_address(program_counter, target_address, core)?
            }
            None => {
                return Err(DebugError::WarnAndContinue {
                    message: "Unable to determine target address for this step request."
                        .to_string(),
                });
            }
        };
        Ok((core_status, program_counter))
    }

    /// To understand how this method works, use the following framework:
    /// - Everything is calculated from a given machine instruction address, usually the current program counter.
    /// - To calculate where the user might step to (step-over, step-into, step-out), we start from the given instruction
    ///     address/program counter, and work our way through all the rows in the sequence of instructions it is part of.
    ///   - A sequence of instructions represents a series of monotonically increasing target machine instructions,
    ///     and does not necessarily represent the whole of a function.
    ///   - Similarly, the instructions belonging to a sequence are not necessarily contiguous inside the sequence of instructions,
    ///     e.g. conditional branching inside the sequence.
    /// - To determine valid halt points for breakpoints and stepping, we only use instructions that qualify as:
    ///   - The beginning of a statement that is neither inside the prologue, nor inside the epilogue.
    /// - Based on this, we will attempt to return the "most appropriate" address for the [`SteppingMode`], given the available information in the instruction sequence.
    ///
    /// All data is calculated using the [`gimli::read::CompleteLineProgram`] as well as, function call data from the debug info frame section.
    ///
    /// 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
    /// 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
    /// and try again, with a reasonable retry limit. All other error kinds are should be treated as non recoverable errors.
    pub(crate) fn get_halt_location(
        &self,
        core: &mut impl CoreInterface,
        debug_info: &DebugInfo,
        program_counter: u64,
        return_address: Option<u64>,
    ) -> Result<VerifiedBreakpoint, DebugError> {
        let program_unit = debug_info.compile_unit_info(program_counter)?;
        match self {
            SteppingMode::BreakPoint => {
                // Find the first_breakpoint_address
                return VerifiedBreakpoint::for_address(debug_info, program_counter);
            }
            SteppingMode::OverStatement => {
                // Find the "step over location"
                // - The instructions in a sequence do not necessarily have contiguous addresses,
                //   and the next instruction address may be affected by conditonal branching at runtime.
                // - Therefore, in order to find the correct "step over location", we iterate through the
                //   instructions to find the starting address of the next halt location, ie. the address
                //   is greater than the current program counter.
                //    -- If there is one, it means the step over target is in the current sequence,
                //       so we get the valid breakpoint location for this next location.
                //    -- If there is not one, the step over target is the same as the step out target.
                return VerifiedBreakpoint::for_address(
                    debug_info,
                    program_counter.saturating_add(1),
                )
                .or_else(|_| {
                    // If we cannot find a valid breakpoint in the current sequence, we will step out of the current sequence.
                    SteppingMode::OutOfStatement.get_halt_location(
                        core,
                        debug_info,
                        program_counter,
                        return_address,
                    )
                });
            }
            SteppingMode::IntoStatement => {
                // 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.
                // - It is not a mandatory attribute, so not sure if we can ever expect it.
                // To find if any functions are called from the current program counter:
                // 1. Identify the next instruction location after the instruction corresponding to the current PC,
                // 2. Single step the target core, until either of the following:
                //   (a) We hit a PC that is NOT in the range between the current PC and the next instruction location.
                //       This location, which could be any of the following:
                //          (a.i)  A legitimate branch outside the current sequence (call to another instruction) such as
                //                 an explicit call to a function, or something the compiler injected, like a `drop()`,
                //          (a.ii) An interrupt handler diverted the processing.
                //   (b) We hit a PC at the address of the identified next instruction location,
                //       which means there was nothing to step into, so the target is now halted (correctly) at the next statement.
                let target_pc = match VerifiedBreakpoint::for_address(
                    debug_info,
                    program_counter.saturating_add(1),
                ) {
                    Ok(identified_next_breakpoint) => identified_next_breakpoint.address,
                    Err(DebugError::WarnAndContinue { .. }) => {
                        // There are no next statements in this sequence, so we will use the return address as the target.
                        if let Some(return_address) = return_address {
                            return_address
                        } else {
                            return Err(DebugError::WarnAndContinue {
                                message: "Could not determine a 'step in' target. Please use 'step over'.".to_string(),
                            });
                        }
                    }
                    Err(other_error) => {
                        return Err(other_error);
                    }
                };

                let (core_status, new_pc) = step_to_address(program_counter..=target_pc, core)?;
                if (program_counter..=target_pc).contains(&new_pc) {
                    // We have halted at an address after the current instruction (either in the same sequence,
                    // or at the return address of the current function),
                    // so we can conclude there were no branching calls in this instruction.
                    tracing::debug!("Stepping into next statement, but no branching calls found. Stepped to next available location.");
                } else if matches!(core_status, CoreStatus::Halted(HaltReason::Breakpoint(_))) {
                    // We have halted at a PC that is within the current statement, so there must be another breakpoint.
                    tracing::debug!("Stepping into next statement, but encountered a breakpoint.");
                } else {
                    tracing::debug!("Stepping into next statement at address: {:#010x}.", new_pc);
                }

                return SteppingMode::BreakPoint.get_halt_location(core, debug_info, new_pc, None);
            }
            SteppingMode::OutOfStatement => {
                if let Ok(function_dies) =
                    program_unit.get_function_dies(debug_info, program_counter)
                {
                    // We want the first qualifying (PC is in range) function from the back of this list,
                    // to access the 'innermost' functions first.
                    if let Some(function) = function_dies.iter().next_back() {
                        tracing::trace!(
                            "Step Out target: Evaluating function {:?}, low_pc={:?}, high_pc={:?}",
                            function.function_name(debug_info),
                            function.low_pc(),
                            function.high_pc()
                        );

                        if function
                            .attribute(debug_info, gimli::DW_AT_noreturn)
                            .is_some()
                        {
                            return Err(DebugError::Other(format!(
                                "Function {:?} is marked as `noreturn`. Cannot step out of this function.",
                                function.function_name(debug_info).as_deref().unwrap_or("<unknown>")
                            )));
                        } else if function.range_contains(program_counter) {
                            if function.is_inline() {
                                // Step_out_address for inlined functions, is the first available breakpoint address after the last statement in the inline function.
                                let (_, next_instruction_address) = run_to_address(
                                    program_counter,
                                    function.high_pc().unwrap(), //unwrap is OK because `range_contains` is true.
                                    core,
                                )?;
                                return SteppingMode::BreakPoint.get_halt_location(
                                    core,
                                    debug_info,
                                    next_instruction_address,
                                    None,
                                );
                            } else if let Some(return_address) = return_address {
                                tracing::debug!(
                                        "Step Out target: non-inline function, stepping over return address: {:#010x}",
                                            return_address
                                    );
                                // Step_out_address for non-inlined functions is the first available breakpoint address after the return address.
                                return SteppingMode::BreakPoint.get_halt_location(
                                    core,
                                    debug_info,
                                    return_address,
                                    None,
                                );
                            }
                        }
                    }
                }
            }
            _ => {
                // SteppingMode::StepInstruction is handled in the `step()` method.
            }
        }

        Err(DebugError::WarnAndContinue {
                message: "Could not determine valid halt locations for this request. Please consider using instruction level stepping.".to_string()
        })
    }
}

/// Run the target to the desired address. If available, we will use a breakpoint, otherwise we will use single step.
/// Returns the program counter at the end of the step, when any of the following conditions are met:
/// - We reach the `target_address_range.end()` (inclusive)
/// - 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")
/// - We encounter an error (e.g. the core locks up, or the USB cable is unplugged, etc.)
/// - It turns out this step will be long-running, and we do not have to wait any longer for the request to complete.
fn run_to_address(
    mut program_counter: u64,
    target_address: u64,
    core: &mut impl CoreInterface,
) -> Result<(CoreStatus, u64), DebugError> {
    Ok(if target_address == program_counter {
        // No need to step further. e.g. For inline functions we have already stepped to the best available target address..
        (
            core.status()?,
            core.read_core_reg(core.program_counter().id())?
                .try_into()?,
        )
    } else if core.set_hw_breakpoint(0, target_address).is_ok() {
        core.run()?;
        // It is possible that we are stepping over long running instructions.
        match core.wait_for_core_halted(Duration::from_millis(1000)) {
            Ok(()) => {
                // We have hit the target address, so all is good.
                // 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.)
                //       This is a legitimate situation, so we clear the breakpoint at the target address, and pass control back to the user
                core.clear_hw_breakpoint(0)?;
                (
                    core.status()?,
                    core.read_core_reg(core.program_counter().id())?
                        .try_into()?,
                )
            }
            Err(error) => {
                program_counter = core.halt(Duration::from_millis(500))?.pc;
                core.clear_hw_breakpoint(0)?;
                if matches!(
                    error,
                    crate::Error::Arm(ArmError::Timeout)
                        | crate::Error::Riscv(RiscvError::Timeout)
                        | crate::Error::Xtensa(XtensaError::Timeout)
                ) {
                    // 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.
                    tracing::error!(
                        "The core did not halt after stepping to {:#010X}. Forced a halt at {:#010X}. Long running operations between debug steps are not currently supported.",
                        target_address,
                        program_counter
                    );
                    (core.status()?, program_counter)
                } else {
                    // Something else is wrong.
                    return Err(DebugError::Other(format!(
                        "Unexpected error while waiting for the core to halt after stepping to {:#010X}. Forced a halt at {:#010X}. {:?}.",
                        program_counter,
                        target_address,
                        error
                    )));
                }
            }
        }
    } else {
        // If we don't have breakpoints to use, we have to rely on single stepping.
        // 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?
        step_to_address(target_address..=u64::MAX, core)?
    })
}

/// In some cases, we need to single-step the core, until ONE of the following conditions are met:
/// - We reach the `target_address_range.end()`
/// - We reach an address that is not in the sequential range of `target_address_range`,
///     i.e. we stepped to some kind of branch instruction, or diversion to an interrupt handler.
/// - 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")
/// - We encounter an error (e.g. the core locks up).
fn step_to_address(
    target_address_range: RangeInclusive<u64>,
    core: &mut impl CoreInterface,
) -> Result<(CoreStatus, u64), DebugError> {
    while target_address_range.contains(&core.step()?.pc) {
        // Single step the core until we get to the target_address;
        match core.status()? {
            CoreStatus::Halted(halt_reason) => match halt_reason {
                HaltReason::Step | HaltReason::Request => continue,
                HaltReason::Breakpoint(_) => {
                    tracing::debug!(
                        "Encountered a breakpoint before the target address ({:#010x}) was reached.",
                        target_address_range.end()
                    );
                    break;
                }
                // This is a recoverable error kind, and can be reported to the user higher up in the call stack.
                other_halt_reason => return Err(DebugError::WarnAndContinue {
                    message: format!("Target halted unexpectedly before we reached the destination address of a step operation: {other_halt_reason:?}")
                }),
            },
            // This is not a recoverable error, and will result in the debug session ending (we have no predicatable way of successfully continuing the session)
            other_status => return Err(DebugError::Other(
                format!("Target failed to reach the destination address of a step operation: {:?}", other_status))
            ),
        }
    }
    Ok((
        core.status()?,
        core.read_core_reg(core.program_counter().id())?
            .try_into()?,
    ))
}