1use std::collections::{BTreeSet, HashMap};
4use std::hash::BuildHasher;
5
6use gimli::{DwarfSections, EndianSlice, Reader, RunTimeEndian};
7use object::{Endianness, Object, ObjectKind, ObjectSection};
8
9use crate::{ArtifactFingerprint, ArtifactInlineFrame, ArtifactIr, ArtifactSourceMapping};
10
11const MAX_DWARF_DEBUG_BYTES: u64 = 64 * 1024 * 1024;
12
13#[allow(
18 clippy::too_many_lines,
19 reason = "DWARF collection and the local address-to-symbol join form one parser boundary"
20)]
21pub fn attach_dwarf_frames<S: BuildHasher>(
22 file: &object::File<'_>,
23 symbol_addresses: &HashMap<ArtifactFingerprint, (u64, u64), S>,
24 ir: &mut ArtifactIr,
25) {
26 if !supports_address_join(file.kind()) {
27 return;
28 }
29 let endian = match file.endianness() {
30 Endianness::Little => RunTimeEndian::Little,
31 Endianness::Big => RunTimeEndian::Big,
32 };
33 let mut remaining_debug_bytes = MAX_DWARF_DEBUG_BYTES;
34 let mut debug_info_unreadable = false;
35 let Ok(sections) = DwarfSections::load(|id| {
36 let Some(section) = debug_section(file, id.name()) else {
37 return Ok::<_, gimli::Error>(Vec::new());
38 };
39 let Ok(data) = section.compressed_data() else {
40 debug_info_unreadable = true;
41 return Ok(Vec::new());
42 };
43 if data.uncompressed_size > remaining_debug_bytes {
44 debug_info_unreadable = true;
45 return Ok(Vec::new());
46 }
47 let Ok(data) = data.decompress() else {
48 debug_info_unreadable = true;
49 return Ok(Vec::new());
50 };
51 remaining_debug_bytes -= data.len() as u64;
52 Ok(data.into_owned())
53 }) else {
54 return;
55 };
56 ir.capabilities.debug_info_unreadable |= debug_info_unreadable;
57 let dwarf = sections.borrow(|section| EndianSlice::new(section, endian));
58 let mut frames = Vec::new();
59 let mut line_records = Vec::new();
60 let mut line_paths = DwarfPathInterner::default();
61 let mut units = dwarf.units();
62 loop {
63 let header = match units.next() {
64 Ok(Some(header)) => header,
65 Ok(None) => break,
66 Err(_) => {
67 ir.capabilities.debug_info_unreadable = true;
68 break;
69 }
70 };
71 let Ok(unit) = dwarf.unit(header) else {
72 ir.capabilities.debug_info_unreadable = true;
73 continue;
74 };
75 let mut entries = unit.entries();
76 let mut depth = 0isize;
77 loop {
78 let entry = match entries.next_dfs() {
79 Ok(Some(entry)) => entry,
80 Ok(None) => break,
81 Err(_) => {
82 ir.capabilities.debug_info_unreadable = true;
83 break;
84 }
85 };
86 let (delta, entry) = entry;
87 depth += delta;
88 if !matches!(
89 entry.tag(),
90 gimli::DW_TAG_subprogram | gimli::DW_TAG_inlined_subroutine
91 ) {
92 continue;
93 }
94 let Some(frame) = source_frame(&dwarf, &unit, entry) else {
95 continue;
96 };
97 let Ok(mut ranges) = dwarf.die_ranges(&unit, entry) else {
98 ir.capabilities.debug_info_unreadable = true;
99 continue;
100 };
101 loop {
102 let range = match ranges.next() {
103 Ok(Some(range)) => range,
104 Ok(None) => break,
105 Err(_) => {
106 ir.capabilities.debug_info_unreadable = true;
107 break;
108 }
109 };
110 if range.begin < range.end {
111 frames.push(DwarfFrame {
112 begin: range.begin,
113 end: range.end,
114 depth,
115 frame: frame.clone(),
116 });
117 }
118 }
119 }
120 line_records.extend(line_frames(&dwarf, &unit, &mut line_paths));
121 }
122 if frames.is_empty() && line_records.is_empty() {
123 return;
124 }
125 frames.sort_by_key(|frame| frame.begin);
126 line_records.sort_by_key(|frame| frame.address);
127 let mut symbols: Vec<_> = symbol_addresses
128 .iter()
129 .map(|(fingerprint, (address, size))| (*fingerprint, *address, *size))
130 .collect();
131 symbols.sort_by_key(|(_, address, _)| *address);
132 let frame_matches = frames_at_symbol_addresses(&frames, &symbols);
133 let symbol_rows: HashMap<_, _> = ir
134 .symbols
135 .iter()
136 .enumerate()
137 .map(|(index, symbol)| (symbol.fingerprint, index))
138 .collect();
139 let mut source_paths = BTreeSet::new();
140 for ((fingerprint, address, size), frame_indexes) in symbols.into_iter().zip(frame_matches) {
141 let mut matching: Vec<_> = frame_indexes
142 .into_iter()
143 .map(|index| (frames[index].depth, frames[index].frame.clone()))
144 .collect();
145 let symbol_end = address.saturating_add(size);
146 let line_start = line_records.partition_point(|candidate| candidate.address < address);
147 matching.extend(
148 line_records
149 .get(line_start..)
150 .into_iter()
151 .flatten()
152 .take_while(|candidate| candidate.address < symbol_end)
153 .map(|candidate| (isize::MAX, candidate.inline_frame(&line_paths))),
154 );
155 matching.sort_by(|left, right| {
156 (&left.1.source, left.1.line, left.1.column, left.0).cmp(&(
157 &right.1.source,
158 right.1.line,
159 right.1.column,
160 right.0,
161 ))
162 });
163 matching.dedup_by(|left, right| left.1 == right.1);
164 if matching.is_empty() {
165 continue;
166 }
167 if let Some(index) = symbol_rows.get(&fingerprint) {
168 let symbol = &mut ir.symbols[*index];
169 symbol.inline_stack = matching.into_iter().map(|(_, frame)| frame).collect();
170 source_paths.extend(symbol.inline_stack.iter().map(|frame| frame.source.clone()));
171 }
172 }
173 ir.source_mappings.extend(
174 source_paths
175 .into_iter()
176 .map(|uri| ArtifactSourceMapping { uri }),
177 );
178}
179
180const fn supports_address_join(kind: ObjectKind) -> bool {
181 !matches!(kind, ObjectKind::Relocatable)
182}
183
184fn frames_at_symbol_addresses(
185 frames: &[DwarfFrame],
186 symbols: &[(ArtifactFingerprint, u64, u64)],
187) -> Vec<Vec<usize>> {
188 let mut active = BTreeSet::new();
189 let mut next = 0;
190 symbols
191 .iter()
192 .map(|(_, address, _)| {
193 while next < frames.len() && frames[next].begin <= *address {
194 active.insert((frames[next].end, next));
195 next += 1;
196 }
197 while active.first().is_some_and(|(end, _)| *end <= *address) {
198 let _ = active.pop_first();
199 }
200 active.iter().map(|(_, index)| *index).collect()
201 })
202 .collect()
203}
204
205fn debug_section<'data, 'file>(
206 file: &'file object::File<'data>,
207 name: &str,
208) -> Option<object::Section<'data, 'file>> {
209 file.section_by_name(name).or_else(|| {
210 let macho_name: String = format!("__{}", name.trim_start_matches('.'))
211 .chars()
212 .take(16)
213 .collect();
214 file.section_by_name(&macho_name)
215 })
216}
217
218#[derive(Debug, Clone)]
219struct DwarfFrame {
220 begin: u64,
221 end: u64,
222 depth: isize,
223 frame: ArtifactInlineFrame,
224}
225
226#[derive(Debug, Clone)]
227struct DwarfLineFrame {
228 address: u64,
229 source: u32,
230 line: u32,
231 column: u32,
233}
234
235impl DwarfLineFrame {
236 fn inline_frame(&self, paths: &DwarfPathInterner) -> ArtifactInlineFrame {
237 ArtifactInlineFrame {
238 evidence_kind: crate::ArtifactSourceLocationEvidenceKind::Dwarf,
239 source: paths.get(self.source).to_owned(),
240 line: Some(self.line),
241 column: (self.column != 0).then_some(self.column),
242 }
243 }
244}
245
246#[derive(Debug, Default)]
248struct DwarfPathInterner {
249 indexes: HashMap<String, usize>,
250 values: Vec<String>,
251}
252
253impl DwarfPathInterner {
254 fn intern(&mut self, path: String) -> Option<u32> {
255 if let Some(index) = self.indexes.get(&path) {
256 return u32::try_from(*index).ok();
257 }
258 let index = self.values.len();
259 let index = u32::try_from(index).ok()?;
260 self.values.push(path.clone());
261 self.indexes.insert(path, index as usize);
262 Some(index)
263 }
264
265 fn get(&self, index: u32) -> &str {
266 self.values
267 .get(index as usize)
268 .map_or("<invalid-dwarf-path>", String::as_str)
269 }
270}
271
272fn line_frames<R: Reader>(
273 dwarf: &gimli::Dwarf<R>,
274 unit: &gimli::Unit<R>,
275 paths: &mut DwarfPathInterner,
276) -> Vec<DwarfLineFrame> {
277 let Some(program) = unit.line_program.clone() else {
278 return Vec::new();
279 };
280 let compilation_directory = unit.comp_dir.as_ref().and_then(reader_string);
281 let mut rows = program.rows();
282 let mut frames = Vec::new();
283 while let Ok(Some((header, row))) = rows.next_row() {
284 if row.end_sequence() {
285 continue;
286 }
287 let Some(line) = row.line().and_then(|value| u32::try_from(value.get()).ok()) else {
288 continue;
289 };
290 let Some(file) = row.file(header) else {
291 continue;
292 };
293 let Some(source) = dwarf
294 .attr_string(unit, file.path_name())
295 .ok()
296 .and_then(|value| reader_string(&value))
297 else {
298 continue;
299 };
300 let directory = file
301 .directory(header)
302 .and_then(|value| dwarf.attr_string(unit, value).ok())
303 .and_then(|value| reader_string(&value));
304 let column = match row.column() {
305 gimli::ColumnType::LeftEdge => None,
306 gimli::ColumnType::Column(value) => u32::try_from(value.get()).ok(),
307 };
308 let Some(source) = paths.intern(resolve_source_path(
309 &source,
310 directory.as_deref(),
311 compilation_directory.as_deref(),
312 )) else {
313 continue;
314 };
315 frames.push(DwarfLineFrame {
316 address: row.address(),
317 source,
318 line,
319 column: column.unwrap_or(0),
320 });
321 }
322 frames
323}
324
325fn source_frame<R: Reader>(
326 dwarf: &gimli::Dwarf<R>,
327 unit: &gimli::Unit<R>,
328 entry: &gimli::DebuggingInformationEntry<'_, '_, R>,
329) -> Option<ArtifactInlineFrame> {
330 let attributes = if entry.tag() == gimli::DW_TAG_inlined_subroutine {
331 (
332 gimli::DW_AT_call_file,
333 gimli::DW_AT_call_line,
334 gimli::DW_AT_call_column,
335 )
336 } else {
337 (
338 gimli::DW_AT_decl_file,
339 gimli::DW_AT_decl_line,
340 gimli::DW_AT_decl_column,
341 )
342 };
343 let file_index = entry
344 .attr_value(attributes.0)
345 .ok()
346 .flatten()
347 .and_then(|value| value.udata_value())?;
348 let line_program = unit.line_program.as_ref()?;
349 let file = line_program.header().file(file_index)?;
350 let source = dwarf
351 .attr_string(unit, file.path_name())
352 .ok()
353 .and_then(|value| reader_string(&value))?;
354 let directory = file
355 .directory(line_program.header())
356 .and_then(|value| dwarf.attr_string(unit, value).ok())
357 .and_then(|value| reader_string(&value));
358 let compilation_directory = unit.comp_dir.as_ref().and_then(reader_string);
359 let line = entry
360 .attr_value(attributes.1)
361 .ok()
362 .flatten()
363 .and_then(|value| value.udata_value())
364 .and_then(|value| u32::try_from(value).ok());
365 let column = entry
366 .attr_value(attributes.2)
367 .ok()
368 .flatten()
369 .and_then(|value| value.udata_value())
370 .and_then(|value| u32::try_from(value).ok());
371 Some(ArtifactInlineFrame {
372 evidence_kind: crate::ArtifactSourceLocationEvidenceKind::Dwarf,
373 source: resolve_source_path(
374 &source,
375 directory.as_deref(),
376 compilation_directory.as_deref(),
377 ),
378 line,
379 column,
380 })
381}
382
383fn reader_string<R: Reader>(value: &R) -> Option<String> {
384 value
385 .to_string_lossy()
386 .ok()
387 .map(std::borrow::Cow::into_owned)
388}
389
390#[must_use]
392pub fn resolve_source_path(
393 file: &str,
394 directory: Option<&str>,
395 compilation_directory: Option<&str>,
396) -> String {
397 fn rooted(path: &str) -> bool {
398 path.starts_with('/')
399 }
400 fn under(base: &str, path: &str) -> String {
401 format!("{}/{path}", base.trim_end_matches('/'))
402 }
403 if rooted(file) {
404 return file.to_string();
405 }
406 let base = match directory {
407 Some(directory) if rooted(directory) => Some(directory.to_string()),
408 Some(directory) => compilation_directory.map(|root| under(root, directory)),
409 None => compilation_directory.map(ToString::to_string),
410 };
411 base.map_or_else(|| file.to_string(), |base| under(&base, file))
412}
413
414#[cfg(test)]
415mod tests {
416 use super::{
417 DwarfFrame, DwarfLineFrame, DwarfPathInterner, debug_section, frames_at_symbol_addresses,
418 resolve_source_path, supports_address_join,
419 };
420 use crate::{ArtifactFingerprint, ArtifactInlineFrame, ArtifactSourceLocationEvidenceKind};
421 use object::ObjectKind;
422
423 fn frame(begin: u64, end: u64) -> DwarfFrame {
424 DwarfFrame {
425 begin,
426 end,
427 depth: 0,
428 frame: ArtifactInlineFrame {
429 evidence_kind: ArtifactSourceLocationEvidenceKind::Dwarf,
430 source: "fixture.rs".to_owned(),
431 line: Some(1),
432 column: None,
433 },
434 }
435 }
436
437 #[test]
438 fn frame_join_advances_a_sweep_index_without_rescanning_all_frames() {
439 let frames = [frame(10, 30), frame(20, 25), frame(40, 50)];
440 let symbols = [
441 (ArtifactFingerprint::from_content("test", b"first"), 12, 4),
442 (ArtifactFingerprint::from_content("test", b"second"), 22, 2),
443 (ArtifactFingerprint::from_content("test", b"third"), 45, 3),
444 ];
445 assert_eq!(
446 frames_at_symbol_addresses(&frames, &symbols),
447 vec![vec![0], vec![1, 0], vec![2]]
448 );
449 }
450
451 #[test]
452 fn relocatable_objects_reject_address_only_dwarf_joins() {
453 assert!(!supports_address_join(ObjectKind::Relocatable));
454 assert!(supports_address_join(ObjectKind::Executable));
455 assert!(supports_address_join(ObjectKind::Dynamic));
456 }
457
458 #[test]
459 fn relative_paths_keep_their_declared_directory_context_without_reading_source() {
460 assert_eq!(
461 resolve_source_path("src/main.cpp", None, Some("/work/tree")),
462 "/work/tree/src/main.cpp"
463 );
464 assert_eq!(
465 resolve_source_path("header.hpp", Some("include"), Some("/work/tree")),
466 "/work/tree/include/header.hpp"
467 );
468 assert_eq!(
469 resolve_source_path("entry.cpp", Some("/other/build"), Some("/work/tree")),
470 "/other/build/entry.cpp"
471 );
472 assert_eq!(
473 resolve_source_path("/outside/entry.cpp", Some("include"), Some("/work/tree")),
474 "/outside/entry.cpp"
475 );
476 assert_eq!(
477 resolve_source_path("src/main.cpp", None, Some("/work/tree/")),
478 "/work/tree/src/main.cpp"
479 );
480 assert_eq!(
481 resolve_source_path("header.hpp", Some("include/"), Some("/work/tree/")),
482 "/work/tree/include/header.hpp"
483 );
484 }
485
486 #[test]
487 #[allow(clippy::unwrap_used)]
488 fn line_records_intern_repeated_paths_until_they_are_attached_to_symbols() {
489 let mut paths = DwarfPathInterner::default();
490 let first = paths.intern("/work/src/lib.rs".to_owned()).unwrap();
491 let second = paths.intern("/work/src/lib.rs".to_owned()).unwrap();
492 let frame = DwarfLineFrame {
493 address: 12,
494 source: first,
495 line: 5,
496 column: 0,
497 };
498
499 assert_eq!(first, second);
500 assert_eq!(paths.values.len(), 1);
501 assert_eq!(frame.inline_frame(&paths).source, "/work/src/lib.rs");
502 }
503
504 #[test]
505 #[allow(clippy::unwrap_used)]
506 fn compressed_legacy_debug_sections_are_found_and_decompressed() {
507 use std::io::Write;
508
509 use flate2::{Compression, write::ZlibEncoder};
510 use object::write::{Object as WriteObject, StandardSegment};
511 use object::{Architecture, BinaryFormat, Endianness, ObjectSection, SectionKind};
512
513 let payload = b"compressed dwarf fixture";
514 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
515 encoder.write_all(payload).unwrap();
516 let compressed = encoder.finish().unwrap();
517 let mut section_data = b"ZLIB".to_vec();
518 section_data.extend_from_slice(&(payload.len() as u64).to_be_bytes());
519 section_data.extend_from_slice(&compressed);
520
521 let mut writer =
522 WriteObject::new(BinaryFormat::Elf, Architecture::X86_64, Endianness::Little);
523 let section = writer.add_section(
524 writer.segment_name(StandardSegment::Debug).to_vec(),
525 b".zdebug_info".to_vec(),
526 SectionKind::Debug,
527 );
528 writer.append_section_data(section, §ion_data, 1);
529 let bytes = writer.write().unwrap();
530 let file = object::File::parse(bytes.as_slice()).unwrap();
531 let section = debug_section(&file, ".debug_info").unwrap();
532
533 assert_eq!(section.uncompressed_data().unwrap().as_ref(), payload);
534 }
535}