Skip to main content

probe_rs_debug/
source_instructions.rs

1use super::{
2    ColumnType, DebugError, DebugInfo, GimliReader, path_matches,
3    unit_info::{self, UnitInfo},
4};
5use gimli::LineSequence;
6use serde::Serialize;
7use std::{
8    fmt::{Debug, Formatter},
9    num::NonZeroU64,
10    ops::Range,
11};
12use typed_path::{TypedPath, TypedPathBuf};
13
14/// A verified breakpoint represents an instruction address, and the source location that it corresponds to it,
15/// for locations in the target binary that comply with the DWARF standard terminology for "recommended breakpoint location".
16/// This typically refers to instructions that are not part of the prologue or epilogue, and are part of the user code,
17/// or are the final instruction in a sequence, before the processor begins the epilogue code.
18/// The `probe-rs` debugger uses this information to identify valid halt locations for breakpoints and stepping.
19#[derive(Clone, Debug)]
20pub struct VerifiedBreakpoint {
21    /// The address in target memory, where the breakpoint can be set.
22    pub address: u64,
23    /// If the breakpoint request was for a specific source location, then this field will contain the resolved source location.
24    pub source_location: SourceLocation,
25}
26
27impl VerifiedBreakpoint {
28    /// Return the first valid breakpoint location of the statement that is greater than OR equal to `address`.
29    /// e.g., if the `address` is the current program counter, then the return value will be the next valid halt address
30    /// in the current sequence.
31    pub(crate) fn for_address(
32        debug_info: &DebugInfo,
33        address: u64,
34    ) -> Result<VerifiedBreakpoint, DebugError> {
35        let instruction_sequence = InstructionSequence::from_address(debug_info, address)?;
36
37        // Cycle through various degrees of matching, to find the most relevant source location.
38        if let Some(verified_breakpoint) = match_address(&instruction_sequence, address, debug_info)
39        {
40            tracing::debug!(
41                "Found valid breakpoint for address: {:#010x} : {verified_breakpoint:?}",
42                &address
43            );
44            return Ok(verified_breakpoint);
45        }
46        // If we get here, we have not found a valid breakpoint location.
47        let message = format!(
48            "Could not identify a valid breakpoint for address: {address:#010x}. Please consider using instruction level stepping."
49        );
50        Err(DebugError::WarnAndContinue { message })
51    }
52
53    /// Identifying the breakpoint location for a specific location (path, line, column) is a bit more complex,
54    /// compared to the `for_address()` method, due to a few factors:
55    /// - The correct program instructions, may be in any of the compilation units of the current program.
56    /// - The debug information may not contain data for the "specific source" location requested:
57    ///   - DWARFv5 standard, section 6.2, allows omissions based on certain conditions. In this case,
58    ///     we need to find the closest "relevant" source location that has valid debug information.
59    ///   - The requested location may not be a valid source location, e.g. when the
60    ///     debug information has been optimized away. In this case we will return an appropriate error.
61    ///
62    /// #### Path matching
63    /// `path` may be an absolute path or a **partial (relative) path**.  A relative path such as
64    /// `src/main.rs` or simply `main.rs` is matched against the tail of each DWARF-embedded
65    /// absolute path at a component boundary, so the caller does not need to know the full path
66    /// that was recorded at compile time.  When `path` is absolute, only normalized exact equality
67    /// is accepted (the previous behaviour).
68    ///
69    /// #### The logic used to find the "most relevant" source location is as follows:
70    /// 1. Filter  [`UnitInfo`], by using [`gimli::LineProgramHeader`] to match units that include
71    ///    the requested path.
72    /// 2. For each matching compilation unit, get the [`gimli::LineProgram`] and
73    ///    [`Vec<LineSequence>`][LineSequence].
74    /// 3. Filter the [`Vec<LineSequence>`][LineSequence] entries to only include sequences that match the requested path.
75    /// 3. Convert remaining [`LineSequence`], to [`InstructionSequence`].
76    /// 4. Return the first [`InstructionSequence`] that contains the requested source location.
77    ///    1. This may be an exact match on file/line/column, or,
78    ///    2. Failing an exact match, a match on file/line only.
79    ///    3. Failing that, a match on file only, where the line number is the "next" available instruction,
80    ///       on the next available line of the specified file.
81    pub(crate) fn for_source_location(
82        debug_info: &DebugInfo,
83        path: TypedPath,
84        line: u64,
85        column: Option<u64>,
86    ) -> Result<Self, DebugError> {
87        for program_unit in &debug_info.unit_infos {
88            let Some(ref line_program) = program_unit.unit.line_program else {
89                // Not all compilation units need to have debug line information, so we skip those.
90                continue;
91            };
92
93            let mut num_files = line_program.header().file_names().len();
94
95            // For DWARF version 5, the current compilation file is included in the file names, with index 0.
96            //
97            // For earlier versions, the current compilation file is not included in the file names, but index 0 still refers to it.
98            // To get the correct number of files, we have to add 1 here.
99            if program_unit.unit.header.version() <= 4 {
100                num_files += 1;
101            }
102
103            // There can be multiple file indices which match, due to the inclusion of the current compilation file with index 0.
104            //
105            // At least for DWARF 4 there are cases where the current compilation file is also included in the file names with
106            // a non-zero index.
107            let matching_file_indices: Vec<_> = (0..num_files)
108                .filter_map(|file_index| {
109                    let file_index = file_index as u64;
110
111                    debug_info
112                        .get_path(&program_unit.unit, file_index)
113                        .and_then(|combined_path: TypedPathBuf| {
114                            if path_matches(combined_path.to_path(), path) {
115                                tracing::debug!(
116                                    "Found matching file index: {file_index} for path: {path}",
117                                    file_index = file_index,
118                                    path = path.display()
119                                );
120                                Some(file_index)
121                            } else {
122                                None
123                            }
124                        })
125                })
126                .collect();
127
128            if matching_file_indices.is_empty() {
129                continue;
130            }
131
132            let Ok((complete_line_program, line_sequences)) = line_program.clone().sequences()
133            else {
134                tracing::debug!("Failed to get line sequences for line program");
135                continue;
136            };
137
138            for line_sequence in line_sequences {
139                let instruction_sequence = InstructionSequence::from_line_sequence(
140                    debug_info,
141                    program_unit,
142                    &complete_line_program,
143                    &line_sequence,
144                );
145
146                for matching_file_index in &matching_file_indices {
147                    // Cycle through various degrees of matching, to find the most relevant source location.
148                    if let Some(verified_breakpoint) = match_file_line_column(
149                        &instruction_sequence,
150                        *matching_file_index,
151                        line,
152                        column,
153                        debug_info,
154                        program_unit,
155                    ) {
156                        return Ok(verified_breakpoint);
157                    }
158
159                    if let Some(verified_breakpoint) = match_file_line_first_available_column(
160                        &instruction_sequence,
161                        *matching_file_index,
162                        line,
163                        debug_info,
164                        program_unit,
165                    ) {
166                        return Ok(verified_breakpoint);
167                    }
168                }
169            }
170        }
171        // If we get here, we have not found a valid breakpoint location.
172        Err(DebugError::Other(format!(
173            "No valid breakpoint information found for file: {}, line: {line:?}, column: {column:?}",
174            path.display()
175        )))
176    }
177}
178
179/// Find the valid halt instruction location that is equal to, or greater than, the address.
180fn match_address(
181    instruction_sequence: &InstructionSequence<'_>,
182    address: u64,
183    debug_info: &DebugInfo,
184) -> Option<VerifiedBreakpoint> {
185    if instruction_sequence.address_range.contains(&address) {
186        let instruction_location =
187            instruction_sequence
188                .instructions
189                .iter()
190                .find(|instruction_location| {
191                    instruction_location.instruction_type == InstructionType::HaltLocation
192                        && instruction_location.address >= address
193                })?;
194
195        let source_location = SourceLocation::from_instruction_location(
196            debug_info,
197            instruction_sequence.program_unit,
198            instruction_location,
199        )?;
200
201        Some(VerifiedBreakpoint {
202            address: instruction_location.address,
203            source_location,
204        })
205    } else {
206        None
207    }
208}
209
210/// Find the valid halt instruction location that matches the file, line and column.
211fn match_file_line_column(
212    instruction_sequence: &InstructionSequence<'_>,
213    matching_file_index: u64,
214    line: u64,
215    column: Option<u64>,
216    debug_info: &DebugInfo,
217    program_unit: &UnitInfo,
218) -> Option<VerifiedBreakpoint> {
219    let instruction_location =
220        instruction_sequence
221            .instructions
222            .iter()
223            .find(|instruction_location| {
224                instruction_location.instruction_type == InstructionType::HaltLocation
225                    && matching_file_index == instruction_location.file_index
226                    && NonZeroU64::new(line) == instruction_location.line
227                    && column
228                        .map(ColumnType::Column)
229                        .is_some_and(|col| col == instruction_location.column)
230            })?;
231
232    let source_location =
233        SourceLocation::from_instruction_location(debug_info, program_unit, instruction_location)?;
234
235    Some(VerifiedBreakpoint {
236        address: instruction_location.address,
237        source_location,
238    })
239}
240
241/// Find the first valid halt instruction location that matches the file and line, ignoring column.
242fn match_file_line_first_available_column(
243    instruction_sequence: &InstructionSequence<'_>,
244    matching_file_index: u64,
245    line: u64,
246    debug_info: &DebugInfo,
247    program_unit: &UnitInfo,
248) -> Option<VerifiedBreakpoint> {
249    let instruction_location =
250        instruction_sequence
251            .instructions
252            .iter()
253            .find(|instruction_location| {
254                instruction_location.instruction_type == InstructionType::HaltLocation
255                    && matching_file_index == instruction_location.file_index
256                    && NonZeroU64::new(line) == instruction_location.line
257            })?;
258
259    let source_location =
260        SourceLocation::from_instruction_location(debug_info, program_unit, instruction_location)?;
261
262    Some(VerifiedBreakpoint {
263        address: instruction_location.address,
264        source_location,
265    })
266}
267
268fn serialize_typed_path<S>(path: &TypedPathBuf, serializer: S) -> Result<S::Ok, S::Error>
269where
270    S: serde::Serializer,
271{
272    serializer.serialize_str(&path.to_string_lossy())
273}
274
275/// A specific location in source code.
276/// Each unique line, column, file and directory combination is a unique source location.
277#[derive(Clone, PartialEq, Eq, Serialize)]
278pub struct SourceLocation {
279    /// The path to the source file
280    #[serde(serialize_with = "serialize_typed_path")]
281    pub path: TypedPathBuf,
282    /// The line number in the source file with zero based indexing.
283    pub line: Option<u64>,
284    /// The column number in the source file.
285    pub column: Option<ColumnType>,
286    /// The address of the source location.
287    pub address: Option<u64>,
288}
289
290impl Debug for SourceLocation {
291    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
292        write!(
293            f,
294            "{}:{:?}:{:?}",
295            self.path.to_path().display(),
296            self.line,
297            self.column
298        )
299    }
300}
301
302impl SourceLocation {
303    /// Resolve debug information for a [`InstructionLocation`] and create a [`SourceLocation`].
304    fn from_instruction_location(
305        debug_info: &DebugInfo,
306        program_unit: &unit_info::UnitInfo,
307        instruction_location: &InstructionLocation,
308    ) -> Option<SourceLocation> {
309        debug_info
310            .find_file_and_directory(&program_unit.unit, instruction_location.file_index)
311            .map(|path| SourceLocation {
312                line: instruction_location.line.map(std::num::NonZeroU64::get),
313                column: Some(instruction_location.column),
314                path,
315                address: Some(instruction_location.address),
316            })
317    }
318
319    /// Get the file name of the source file
320    pub fn file_name(&self) -> Option<String> {
321        self.path
322            .file_name()
323            .map(|name| String::from_utf8_lossy(name).to_string())
324    }
325}
326
327/// Keep track of all the instruction locations required to satisfy the operations of [`SteppingMode`][s].
328/// This is a list of target instructions, belonging to a [`gimli::LineSequence`],
329/// and filters it to only user code instructions (no prologue code, and no non-statement instructions),
330/// so that we are left only with what DWARF terms as "recommended breakpoint location".
331///
332/// [s]: crate::debug::debug_step::SteppingMode
333struct InstructionSequence<'debug_info> {
334    /// The `address_range.start` is the starting address of the program counter for which this sequence is valid,
335    /// and allows us to identify target instruction locations where the program counter lies inside the prologue.
336    /// The `address_range.end` is the first address that is not covered by this sequence within the line number program,
337    /// and allows us to identify when stepping over a instruction location would result in leaving a sequence.
338    /// - This is typically the instruction address of the first instruction in the next sequence,
339    ///   which may also be the first instruction in a new function.
340    address_range: Range<u64>,
341    // NOTE: Use Vec as a container, because we will have relatively few statements per sequence, and we need to maintain the order.
342    instructions: Vec<InstructionLocation>,
343    // The following private fields are required to resolve the source location information for
344    // each instruction location.
345    debug_info: &'debug_info DebugInfo,
346    program_unit: &'debug_info UnitInfo,
347}
348
349impl Debug for InstructionSequence<'_> {
350    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
351        writeln!(
352            f,
353            "Instruction Sequence with address range: {:#010x} - {:#010x}",
354            self.address_range.start, self.address_range.end
355        )?;
356        for instruction_location in &self.instructions {
357            writeln!(
358                f,
359                "\t{instruction_location:?} - {}",
360                self.debug_info
361                    .get_path(&self.program_unit.unit, instruction_location.file_index)
362                    .map(|file_path| file_path.to_string_lossy().to_string())
363                    .unwrap_or("<unknown file>".to_string())
364            )?;
365        }
366        Ok(())
367    }
368}
369
370impl<'debug_info> InstructionSequence<'debug_info> {
371    /// Extract all the instruction locations, belonging to the active sequence (i.e. the sequence that contains the `address`).
372    fn from_address(
373        debug_info: &'debug_info DebugInfo,
374        program_counter: u64,
375    ) -> Result<Self, DebugError> {
376        let program_unit = debug_info.compile_unit_info(program_counter)?;
377        let (offset, address_size) = if let Some(line_program) =
378            program_unit.unit.line_program.clone()
379        {
380            (
381                line_program.header().offset(),
382                line_program.header().address_size(),
383            )
384        } else {
385            let message = "The specified source location does not have any line_program information available. Please consider using instruction level stepping.".to_string();
386            return Err(DebugError::WarnAndContinue { message });
387        };
388
389        // Get the sequences of rows from the CompleteLineProgram at the given program_counter.
390        let incomplete_line_program =
391            debug_info
392                .debug_line_section
393                .program(offset, address_size, None, None)?;
394        let (complete_line_program, line_sequences) = incomplete_line_program.sequences()?;
395
396        // Get the sequence of rows that belongs to the program_counter.
397        let Some(line_sequence) = line_sequences.iter().find(|line_sequence| {
398            line_sequence.start <= program_counter && program_counter < line_sequence.end
399        }) else {
400            let message = "The specified source location does not have any line information available. Please consider using instruction level stepping.".to_string();
401            return Err(DebugError::WarnAndContinue { message });
402        };
403        let instruction_sequence = Self::from_line_sequence(
404            debug_info,
405            program_unit,
406            &complete_line_program,
407            line_sequence,
408        );
409
410        if instruction_sequence.len() == 0 {
411            let message = "Could not find valid instruction locations for this address. Consider using instruction level stepping.".to_string();
412            Err(DebugError::WarnAndContinue { message })
413        } else {
414            tracing::trace!(
415                "Instruction location for pc={:#010x}\n{:?}",
416                program_counter,
417                instruction_sequence
418            );
419            Ok(instruction_sequence)
420        }
421    }
422
423    /// Build [`InstructionSequence`] from a [`gimli::LineSequence`], with all the markers we need to determine valid halt locations.
424    fn from_line_sequence(
425        debug_info: &'debug_info DebugInfo,
426        program_unit: &'debug_info UnitInfo,
427        complete_line_program: &gimli::CompleteLineProgram<GimliReader>,
428        line_sequence: &LineSequence<GimliReader>,
429    ) -> Self {
430        let program_language = program_unit.get_language();
431        let mut sequence_rows = complete_line_program.resume_from(line_sequence);
432
433        // We have enough information to create the InstructionSequence.
434        let mut instruction_sequence = InstructionSequence {
435            address_range: line_sequence.start..line_sequence.end,
436            instructions: Vec::new(),
437            debug_info,
438            program_unit,
439        };
440        let mut prologue_completed = false;
441        let mut previous_row: Option<gimli::LineRow> = None;
442        while let Ok(Some((_, row))) = sequence_rows.next_row() {
443            // Don't do anything until we are at least at the prologue_end() of a function.
444            if row.prologue_end() {
445                prologue_completed = true;
446            }
447
448            // For GNU C, it is known that the `DW_LNS_set_prologue_end` is not set, so we employ the same heuristic as GDB to determine when the prologue is complete.
449            // For other C compilers in the C99/11/17 standard, they will either set the `DW_LNS_set_prologue_end` or they will trigger this heuristic also.
450            // See https://gcc.gnu.org/legacy-ml/gcc-patches/2011-03/msg02106.html
451            if !prologue_completed
452                && matches!(
453                    program_language,
454                    gimli::DW_LANG_C99 | gimli::DW_LANG_C11 | gimli::DW_LANG_C17
455                )
456                && let Some(prev_row) = previous_row
457                && (row.end_sequence()
458                    || (row.is_stmt()
459                        && (row.file_index() == prev_row.file_index()
460                            && (row.line() != prev_row.line() || row.line().is_none()))))
461            {
462                prologue_completed = true;
463            }
464
465            if !prologue_completed {
466                log_row_eval(line_sequence, row, "  inside prologue>");
467            } else {
468                log_row_eval(line_sequence, row, "  after prologue>");
469            }
470
471            // The end of the sequence is not a valid halt location,
472            // nor is it a valid instruction in the current sequence.
473            if row.end_sequence() {
474                break;
475            }
476
477            instruction_sequence.add(prologue_completed, row, previous_row.as_ref());
478            previous_row = Some(*row);
479        }
480        instruction_sequence
481    }
482
483    /// Add a instruction location to the list.
484    fn add(
485        &mut self,
486        prologue_completed: bool,
487        row: &gimli::LineRow,
488        previous_row: Option<&gimli::LineRow>,
489    ) {
490        // Workaround the line number issue (if recorded as 0 in the DWARF, then gimli reports it as None).
491        // For debug purposes, it makes more sense to be the same as the previous line, which almost always
492        // has the same file index and column value.
493        // This prevents the debugger from jumping to the top of the file unexpectedly.
494        let mut instruction_line = row.line();
495        if let Some(prev_row) = previous_row
496            && row.line().is_none()
497            && prev_row.line().is_some()
498            && row.file_index() == prev_row.file_index()
499            && prev_row.column() == row.column()
500        {
501            instruction_line = prev_row.line();
502        }
503
504        let instruction_location = InstructionLocation {
505            address: row.address(),
506            file_index: row.file_index(),
507            line: instruction_line,
508            column: row.column().into(),
509            instruction_type: if !prologue_completed {
510                InstructionType::Prologue
511            } else if row.epilogue_begin() || row.is_stmt() {
512                InstructionType::HaltLocation
513            } else {
514                InstructionType::Unspecified
515            },
516        };
517
518        self.instructions.push(instruction_location);
519    }
520
521    /// Get the number of instruction locations in the list.
522    fn len(&self) -> usize {
523        self.instructions.len()
524    }
525}
526
527#[derive(Debug, Clone, Copy, PartialEq)]
528/// The type of instruction, as defined by [`gimli::LineRow`] attributes and relative position in the sequence.
529enum InstructionType {
530    /// We need to keep track of source lines that signal function signatures,
531    /// even if their program lines are not valid halt locations.
532    Prologue,
533    /// DWARF defined "recommended breakpoint location",
534    /// typically marked with `is_stmt` or `epilogue_begin`.
535    HaltLocation,
536    /// Any other instruction that is not part of the prologue or epilogue, and is not a statement,
537    /// is considered to be an unspecified instruction type.
538    Unspecified,
539}
540
541#[derive(Clone, Copy)]
542/// - A [`InstructionLocation`] filters and maps [`gimli::LineRow`] entries to be used for determining valid halt points.
543///   - Each [`InstructionLocation`] maps to a single machine instruction on target.
544///   - For establishing valid halt locations (breakpoint or stepping), we are only interested,
545///     in the [`InstructionLocation`]'s that represent DWARF defined `statements`,
546///     which are not part of the prologue or epilogue.
547/// - A line of code in a source file may contain multiple instruction locations, in which case
548///   a new [`InstructionLocation`] with unique `column` is created.
549/// - A [`InstructionSequence`] is a series of contiguous [`InstructionLocation`]'s.
550struct InstructionLocation {
551    address: u64,
552    file_index: u64,
553    line: Option<NonZeroU64>,
554    column: ColumnType,
555    instruction_type: InstructionType,
556}
557
558impl Debug for InstructionLocation {
559    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
560        write!(
561            f,
562            "Instruction @ {:010x}, on line={:04}  col={:05}  f={:02}, type={:?}",
563            self.address,
564            match self.line {
565                Some(line) => line.get(),
566                None => 0,
567            },
568            match self.column {
569                ColumnType::LeftEdge => 0,
570                ColumnType::Column(column) => column,
571            },
572            self.file_index,
573            self.instruction_type,
574        )
575    }
576}
577
578/// Helper function to avoid code duplication when logging of information during row evaluation.
579fn log_row_eval(
580    active_sequence: &LineSequence<super::GimliReader>,
581    row: &gimli::LineRow,
582    status: &str,
583) {
584    tracing::trace!(
585        "Sequence: line={:04} col={:05} f={:02} stmt={:5} ep={:5} es={:5} eb={:5} : {:#010X}<={:#010X}<{:#010X} : {}",
586        match row.line() {
587            Some(line) => line.get(),
588            None => 0,
589        },
590        match row.column() {
591            gimli::ColumnType::LeftEdge => 0,
592            gimli::ColumnType::Column(column) => column.get(),
593        },
594        row.file_index(),
595        row.is_stmt(),
596        row.prologue_end(),
597        row.end_sequence(),
598        row.epilogue_begin(),
599        active_sequence.start,
600        row.address(),
601        active_sequence.end,
602        status
603    );
604}