1use std::collections::HashMap;
16use std::path::{Path, PathBuf};
17
18#[derive(Debug, Clone)]
20pub struct ResolvedFrame {
21 pub address: usize,
23 pub function: Option<String>,
25 pub library: Option<String>,
27 pub offset: usize,
29 pub file: Option<String>,
31 pub line: Option<u32>,
33}
34
35#[cfg(any(target_os = "macos", target_env = "gnu"))]
41mod dladdr_ffi {
42 use std::ffi::{c_void, CStr};
43 use std::os::raw::c_char;
44 use std::path::Path;
45
46 use super::ResolvedFrame;
47
48 #[repr(C)]
49 struct DlInfo {
50 dli_fname: *const c_char,
51 dli_fbase: *mut c_void,
52 dli_sname: *const c_char,
53 dli_saddr: *mut c_void,
54 }
55
56 extern "C" {
57 fn dladdr(addr: *const c_void, info: *mut DlInfo) -> i32;
58 }
59
60 pub struct DladdrResult {
62 pub frame: ResolvedFrame,
63 pub library_path: Option<String>,
65 pub library_base: usize,
67 }
68
69 pub fn resolve(addr: usize) -> DladdrResult {
71 let mut info: DlInfo = unsafe { std::mem::zeroed() };
72 let ret = unsafe { dladdr(addr as *const c_void, &mut info) };
73 if ret == 0 {
74 return DladdrResult {
75 frame: ResolvedFrame {
76 address: addr,
77 function: None,
78 library: None,
79 offset: 0,
80 file: None,
81 line: None,
82 },
83 library_path: None,
84 library_base: 0,
85 };
86 }
87
88 let function = if info.dli_sname.is_null() {
89 None
90 } else {
91 unsafe { CStr::from_ptr(info.dli_sname) }
92 .to_str()
93 .ok()
94 .map(String::from)
95 };
96
97 let (library, library_path) = if info.dli_fname.is_null() {
98 (None, None)
99 } else {
100 let full_path = unsafe { CStr::from_ptr(info.dli_fname) }
101 .to_str()
102 .ok()
103 .map(String::from);
104 let short_name = full_path.as_deref().map(|s| {
105 Path::new(s)
106 .file_name()
107 .and_then(|f| f.to_str())
108 .unwrap_or(s)
109 .to_string()
110 });
111 (short_name, full_path)
112 };
113
114 let offset = if info.dli_saddr.is_null() {
115 0
116 } else {
117 addr.wrapping_sub(info.dli_saddr as usize)
118 };
119
120 let library_base = info.dli_fbase as usize;
121
122 DladdrResult {
123 frame: ResolvedFrame {
124 address: addr,
125 function,
126 library,
127 offset,
128 file: None,
129 line: None,
130 },
131 library_path,
132 library_base,
133 }
134 }
135}
136
137#[cfg(windows)]
140mod dladdr_ffi {
141 use std::ffi::c_void;
142 use std::path::Path;
143
144 use super::ResolvedFrame;
145
146 const MAX_SYM_NAME: usize = 256;
148
149 #[repr(C)]
150 struct SymbolInfo {
151 size_of_struct: u32,
152 type_index: u32,
153 reserved: [u64; 2],
154 index: u32,
155 size: u32,
156 mod_base: u64,
157 flags: u32,
158 value: u64,
159 address: u64,
160 register: u32,
161 scope: u32,
162 tag: u32,
163 name_len: u32,
164 max_name_len: u32,
165 name: [u8; MAX_SYM_NAME],
166 }
167
168 #[repr(C)]
169 struct ImagehlpLine64 {
170 size_of_struct: u32,
171 key: *mut c_void,
172 line_number: u32,
173 file_name: *const u8,
174 address: u64,
175 }
176
177 #[repr(C)]
178 struct ImagehlpModule64 {
179 size_of_struct: u32,
180 base_of_image: u64,
181 image_size: u32,
182 time_date_stamp: u32,
183 check_sum: u32,
184 num_syms: u32,
185 sym_type: u32,
186 module_name: [u8; 32],
187 image_name: [u8; 256],
188 loaded_image_name: [u8; 256],
189 }
191
192 extern "system" {
193 fn GetCurrentProcess() -> *mut c_void;
194 fn SymInitialize(process: *mut c_void, search_path: *const u8, invade: i32) -> i32;
195 fn SymFromAddr(
196 process: *mut c_void,
197 address: u64,
198 displacement: *mut u64,
199 symbol: *mut SymbolInfo,
200 ) -> i32;
201 fn SymGetLineFromAddr64(
202 process: *mut c_void,
203 address: u64,
204 displacement: *mut u32,
205 line: *mut ImagehlpLine64,
206 ) -> i32;
207 fn SymGetModuleInfo64(
208 process: *mut c_void,
209 address: u64,
210 module_info: *mut ImagehlpModule64,
211 ) -> i32;
212 }
213
214 use std::sync::Once;
215 static DBGHELP_INIT: Once = Once::new();
216
217 fn ensure_dbghelp_init() {
218 DBGHELP_INIT.call_once(|| unsafe {
219 let process = GetCurrentProcess();
220 SymInitialize(process, std::ptr::null(), 1);
221 });
222 }
223
224 pub struct DladdrResult {
225 pub frame: ResolvedFrame,
226 pub library_path: Option<String>,
227 pub library_base: usize,
228 }
229
230 pub fn resolve(addr: usize) -> DladdrResult {
232 ensure_dbghelp_init();
233
234 let process = unsafe { GetCurrentProcess() };
235 let mut function = None;
236 let mut library = None;
237 let mut library_path = None;
238 let mut offset = 0usize;
239 let mut file = None;
240 let mut line = None;
241
242 let mut sym_info: SymbolInfo = unsafe { std::mem::zeroed() };
244 sym_info.size_of_struct =
245 std::mem::size_of::<SymbolInfo>() as u32 - MAX_SYM_NAME as u32 + 1;
246 sym_info.max_name_len = MAX_SYM_NAME as u32;
247 let mut displacement: u64 = 0;
248 if unsafe { SymFromAddr(process, addr as u64, &mut displacement, &mut sym_info) } != 0 {
249 let name_len = sym_info.name_len as usize;
250 if let Ok(name) = std::str::from_utf8(&sym_info.name[..name_len]) {
251 function = Some(name.to_string());
252 }
253 offset = displacement as usize;
254 }
255
256 let mut line_info: ImagehlpLine64 = unsafe { std::mem::zeroed() };
258 line_info.size_of_struct = std::mem::size_of::<ImagehlpLine64>() as u32;
259 let mut line_displacement: u32 = 0;
260 if unsafe {
261 SymGetLineFromAddr64(process, addr as u64, &mut line_displacement, &mut line_info)
262 } != 0
263 {
264 line = Some(line_info.line_number);
265 if !line_info.file_name.is_null() {
266 let c_str = unsafe { std::ffi::CStr::from_ptr(line_info.file_name as *const _) };
267 if let Ok(s) = c_str.to_str() {
268 file = Some(
269 Path::new(s)
270 .file_name()
271 .and_then(|n| n.to_str())
272 .unwrap_or(s)
273 .to_string(),
274 );
275 }
276 }
277 }
278
279 let mut mod_info: ImagehlpModule64 = unsafe { std::mem::zeroed() };
281 mod_info.size_of_struct = std::mem::size_of::<ImagehlpModule64>() as u32;
282 if unsafe { SymGetModuleInfo64(process, addr as u64, &mut mod_info) } != 0 {
283 let nul_pos = mod_info
284 .image_name
285 .iter()
286 .position(|&b| b == 0)
287 .unwrap_or(mod_info.image_name.len());
288 if let Ok(path_str) = std::str::from_utf8(&mod_info.image_name[..nul_pos]) {
289 library_path = Some(path_str.to_string());
290 library = Some(
291 Path::new(path_str)
292 .file_name()
293 .and_then(|n| n.to_str())
294 .unwrap_or(path_str)
295 .to_string(),
296 );
297 }
298 }
299
300 DladdrResult {
301 frame: ResolvedFrame {
302 address: addr,
303 function,
304 library,
305 offset,
306 file,
307 line,
308 },
309 library_path,
310 library_base: 0,
311 }
312 }
313}
314
315#[cfg(not(any(target_os = "macos", target_env = "gnu", windows)))]
317mod dladdr_ffi {
318 use super::ResolvedFrame;
319
320 pub struct DladdrResult {
321 pub frame: ResolvedFrame,
322 pub library_path: Option<String>,
323 pub library_base: usize,
324 }
325
326 pub fn resolve(addr: usize) -> DladdrResult {
327 DladdrResult {
328 frame: ResolvedFrame {
329 address: addr,
330 function: None,
331 library: None,
332 offset: 0,
333 file: None,
334 line: None,
335 },
336 library_path: None,
337 library_base: 0,
338 }
339 }
340}
341
342#[cfg(not(windows))]
353fn find_debug_file(library_path: &str) -> PathBuf {
356 let lib = Path::new(library_path);
357
358 #[cfg(target_os = "macos")]
360 if let Some(dsym) = find_dsym(lib) {
361 return dsym;
362 }
363
364 #[cfg(target_os = "linux")]
366 if let Some(debug) = find_linux_debug_file(lib) {
367 return debug;
368 }
369
370 lib.to_path_buf()
372}
373
374#[cfg(target_os = "macos")]
376fn find_dsym(lib: &Path) -> Option<PathBuf> {
377 let filename = lib.file_name()?.to_str()?;
378 let dsym = lib
379 .parent()?
380 .join(format!("{}.dSYM", filename))
381 .join("Contents")
382 .join("Resources")
383 .join("DWARF")
384 .join(filename);
385 if dsym.exists() {
386 Some(dsym)
387 } else {
388 None
389 }
390}
391
392#[cfg(target_os = "linux")]
394fn find_linux_debug_file(lib: &Path) -> Option<PathBuf> {
395 if let Some(path) = find_gnu_debuglink(lib) {
397 return Some(path);
398 }
399
400 if let Some(path) = find_build_id_debug(lib) {
402 return Some(path);
403 }
404
405 let debug_mirror = Path::new("/usr/lib/debug").join(lib.strip_prefix("/").unwrap_or(lib));
407 if debug_mirror.exists() {
408 return Some(debug_mirror);
409 }
410
411 None
412}
413
414#[cfg(target_os = "linux")]
418fn find_gnu_debuglink(lib: &Path) -> Option<PathBuf> {
419 use object::{Object as _, ObjectSection as _};
420
421 let bytes = std::fs::read(lib).ok()?;
422 let object = object::File::parse(&*bytes).ok()?;
423 let section = object.section_by_name(".gnu_debuglink")?;
424 let data = section.data().ok()?;
425
426 let nul_pos = data.iter().position(|&b| b == 0)?;
429 let debug_filename = std::str::from_utf8(&data[..nul_pos]).ok()?;
430
431 if let Some(dir) = lib.parent() {
433 let candidate = dir.join(debug_filename);
434 if candidate.exists() {
435 return Some(candidate);
436 }
437 let candidate = dir.join(".debug").join(debug_filename);
439 if candidate.exists() {
440 return Some(candidate);
441 }
442 }
443
444 if let Some(dir) = lib.parent() {
446 let candidate = Path::new("/usr/lib/debug")
447 .join(dir.strip_prefix("/").unwrap_or(dir))
448 .join(debug_filename);
449 if candidate.exists() {
450 return Some(candidate);
451 }
452 }
453
454 None
455}
456
457#[cfg(target_os = "linux")]
460fn find_build_id_debug(lib: &Path) -> Option<PathBuf> {
461 use object::{Object as _, ObjectSection as _};
462
463 let bytes = std::fs::read(lib).ok()?;
464 let object = object::File::parse(&*bytes).ok()?;
465
466 let section = object.section_by_name(".note.gnu.build-id")?;
469 let data = section.data().ok()?;
470 if data.len() < 16 {
471 return None;
472 }
473
474 let namesz = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
475 let descsz = u32::from_le_bytes(data[4..8].try_into().ok()?) as usize;
476 let name_end = 12 + ((namesz + 3) & !3);
478 if data.len() < name_end + descsz || descsz < 2 {
479 return None;
480 }
481 let build_id = &data[name_end..name_end + descsz];
482
483 let hex: String = build_id.iter().map(|b| format!("{:02x}", b)).collect();
485 let (dir_part, file_part) = hex.split_at(2);
486 let candidate = Path::new("/usr/lib/debug/.build-id")
487 .join(dir_part)
488 .join(format!("{}.debug", file_part));
489 if candidate.exists() {
490 Some(candidate)
491 } else {
492 None
493 }
494}
495
496#[cfg(not(windows))]
502struct DwarfCache {
506 contexts: HashMap<
508 String,
509 Option<addr2line::Context<gimli::EndianSlice<'static, gimli::RunTimeEndian>>>,
510 >,
511}
512
513#[cfg(not(windows))]
514impl DwarfCache {
515 fn new() -> Self {
516 Self {
517 contexts: HashMap::new(),
518 }
519 }
520
521 fn get_context(
523 &mut self,
524 library_path: &str,
525 ) -> Option<&addr2line::Context<gimli::EndianSlice<'static, gimli::RunTimeEndian>>> {
526 if !self.contexts.contains_key(library_path) {
527 let ctx = Self::load_context(library_path);
528 self.contexts.insert(library_path.to_string(), ctx);
529 }
530 self.contexts.get(library_path).and_then(|opt| opt.as_ref())
531 }
532
533 fn load_context(
537 library_path: &str,
538 ) -> Option<addr2line::Context<gimli::EndianSlice<'static, gimli::RunTimeEndian>>> {
539 let debug_path = find_debug_file(library_path);
540 let bytes = std::fs::read(&debug_path).ok()?;
541 let bytes: &'static [u8] = Vec::leak(bytes);
544
545 use object::Object as _;
546 let object = object::File::parse(bytes).ok()?;
547 let endian = if object.is_little_endian() {
548 gimli::RunTimeEndian::Little
549 } else {
550 gimli::RunTimeEndian::Big
551 };
552
553 let dwarf = gimli::Dwarf::load(|section_id| -> Result<_, gimli::Error> {
554 use object::ObjectSection as _;
555 let data = object
556 .section_by_name(section_id.name())
557 .and_then(|s: object::Section<'_, '_>| s.uncompressed_data().ok())
558 .unwrap_or(std::borrow::Cow::Borrowed(&[]));
559 let slice: &'static [u8] = match data {
560 std::borrow::Cow::Borrowed(b) => b,
561 std::borrow::Cow::Owned(v) => Vec::leak(v),
562 };
563 Ok(gimli::EndianSlice::new(slice, endian))
564 })
565 .ok()?;
566
567 addr2line::Context::from_dwarf(dwarf).ok()
568 }
569}
570
571#[cfg(not(windows))]
572thread_local! {
573 static DWARF_CACHE: std::cell::RefCell<DwarfCache> = std::cell::RefCell::new(DwarfCache::new());
574}
575
576#[cfg(not(windows))]
578fn dwarf_resolve(
579 addr: usize,
580 library_path: &str,
581 library_base: usize,
582) -> (Option<String>, Option<u32>) {
583 let relative_addr = (addr as u64).wrapping_sub(library_base as u64);
585
586 DWARF_CACHE
587 .with(|cache| {
588 let mut cache = cache.borrow_mut();
589 let ctx = cache.get_context(library_path)?;
590
591 if let Ok(Some(loc)) = ctx.find_location(relative_addr) {
592 let file = loc.file.map(|f| {
593 Path::new(f)
595 .file_name()
596 .and_then(|n| n.to_str())
597 .unwrap_or(f)
598 .to_string()
599 });
600 let line = loc.line;
601 Some((file, line))
602 } else {
603 None
604 }
605 })
606 .unwrap_or((None, None))
607}
608
609pub fn resolve_native_backtrace(frames: &[usize]) -> Vec<ResolvedFrame> {
618 let mut dladdr_results: Vec<dladdr_ffi::DladdrResult> = frames
620 .iter()
621 .map(|&addr| dladdr_ffi::resolve(addr))
622 .collect();
623
624 #[cfg(not(windows))]
627 for result in &mut dladdr_results {
628 if let Some(ref lib_path) = result.library_path {
629 if result.frame.file.is_none() {
630 let (file, line) =
631 dwarf_resolve(result.frame.address, lib_path, result.library_base);
632 result.frame.file = file;
633 result.frame.line = line;
634 }
635 }
636 }
637
638 let resolved: Vec<ResolvedFrame> = dladdr_results.into_iter().map(|r| r.frame).collect();
640 let mut result = Vec::new();
641 let mut started = false;
642 for frame in &resolved {
643 let name = frame.function.as_deref().unwrap_or("");
644
645 if !started {
646 if name.contains("Rf_error")
647 || name.contains("Rf_errorcall")
648 || name == "backtrace"
649 || name.contains("longjmp")
650 || name.contains("_sigtramp")
651 {
652 continue;
653 }
654 started = true;
655 }
656
657 if name.contains("_minir_call_protected") || name.contains("_minir_dotC_call_protected") {
658 break;
659 }
660
661 result.push(frame.clone());
662 }
663
664 if result.is_empty() && !resolved.is_empty() {
665 return resolved;
666 }
667
668 result
669}
670
671pub fn format_native_frames(frames: &[ResolvedFrame]) -> String {
673 let mut lines = Vec::with_capacity(frames.len());
674 for frame in frames {
675 let addr_fallback = format!("0x{:x}", frame.address);
676 let func = frame.function.as_deref().unwrap_or(&addr_fallback);
677 let lib = frame.library.as_deref().unwrap_or("???");
678
679 let location = match (&frame.file, frame.line) {
681 (Some(file), Some(line)) => format!(" at {}:{}", file, line),
682 (Some(file), None) => format!(" at {}", file),
683 _ => String::new(),
684 };
685
686 if frame.offset > 0 && location.is_empty() {
687 lines.push(format!(" [C] {}+0x{:x} ({})", func, frame.offset, lib));
688 } else {
689 lines.push(format!(" [C] {}{} ({})", func, location, lib));
690 }
691 }
692 lines.join("\n")
693}
694
695