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}