1#![no_std]
2
3#[cfg(feature = "alloc")]
4extern crate alloc;
5
6#[cfg(feature = "alloc")]
7use alloc::{boxed::Box, vec::Vec};
8use core::{
9 fmt,
10 ops::Range,
11 sync::atomic::{AtomicUsize, Ordering},
12};
13
14use ax_lazyinit::OnceLock;
15
16#[cfg(feature = "dwarf")]
17mod dwarf;
18
19#[cfg(feature = "dwarf")]
20pub use dwarf::{DwarfReader, FrameIter};
21
22static IP_RANGE: OnceLock<Range<usize>> = OnceLock::new();
23static FP_RANGE: OnceLock<Range<usize>> = OnceLock::new();
24
25#[cfg(target_arch = "x86_64")]
26const TARGET_ARCH: &str = "x86_64";
27#[cfg(target_arch = "aarch64")]
28const TARGET_ARCH: &str = "aarch64";
29#[cfg(target_arch = "riscv64")]
30const TARGET_ARCH: &str = "riscv64";
31#[cfg(target_arch = "riscv32")]
32const TARGET_ARCH: &str = "riscv32";
33#[cfg(target_arch = "loongarch64")]
34const TARGET_ARCH: &str = "loongarch64";
35#[cfg(not(any(
36 target_arch = "x86_64",
37 target_arch = "aarch64",
38 target_arch = "riscv64",
39 target_arch = "riscv32",
40 target_arch = "loongarch64"
41)))]
42const TARGET_ARCH: &str = "unknown";
43
44pub fn init(ip_range: Range<usize>, fp_range: Range<usize>) {
46 IP_RANGE.call_once(|| ip_range);
47 FP_RANGE.call_once(|| fp_range);
48 #[cfg(feature = "dwarf")]
49 dwarf::init();
50}
51
52#[repr(C)]
54#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
55pub struct Frame {
56 pub fp: usize,
58 pub ip: usize,
60}
61
62impl Frame {
63 #[cfg(feature = "alloc")]
64 #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
65 const OFFSET: usize = 0;
66 #[cfg(feature = "alloc")]
67 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
68 const OFFSET: usize = 1;
69
70 #[cfg(feature = "alloc")]
71 fn read(fp: usize) -> Option<Self> {
72 if fp == 0 || !fp.is_multiple_of(core::mem::align_of::<Frame>()) {
73 return None;
74 }
75
76 Some(unsafe { (fp as *const Frame).sub(Self::OFFSET).read() })
77 }
78
79 #[cfg(target_arch = "x86_64")]
83 pub fn adjust_ip(&self) -> usize {
84 self.ip.wrapping_sub(1) }
86 #[cfg(target_arch = "aarch64")]
87 pub fn adjust_ip(&self) -> usize {
88 self.ip.wrapping_sub(4) }
90 #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))]
91 pub fn adjust_ip(&self) -> usize {
92 self.ip.wrapping_sub(2) }
94 #[cfg(target_arch = "loongarch64")]
95 pub fn adjust_ip(&self) -> usize {
96 self.ip.wrapping_sub(4) }
98}
99
100impl fmt::Display for Frame {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "fp={:#x}, ip={:#x}", self.fp, self.ip)
103 }
104}
105
106#[cfg(feature = "alloc")]
108const CAPTURE_CAPACITY: usize = 32;
109
110#[cfg(feature = "alloc")]
113#[derive(Clone)]
114struct CaptureBuf {
115 frames: [Frame; CAPTURE_CAPACITY],
116 len: usize,
117}
118
119#[cfg(feature = "alloc")]
120impl CaptureBuf {
121 const EMPTY: Self = Self {
122 frames: [Frame { fp: 0, ip: 0 }; CAPTURE_CAPACITY],
123 len: 0,
124 };
125
126 fn push(&mut self, frame: Frame) -> bool {
127 if self.len < CAPTURE_CAPACITY {
128 self.frames[self.len] = frame;
129 self.len += 1;
130 true
131 } else {
132 false
133 }
134 }
135
136 fn insert_front(&mut self, frame: Frame) {
139 let end = if self.len < CAPTURE_CAPACITY {
140 self.len += 1;
141 self.len
142 } else {
143 CAPTURE_CAPACITY };
145 self.frames.copy_within(0..end - 1, 1);
146 self.frames[0] = frame;
147 }
148
149 fn first_mut(&mut self) -> Option<&mut Frame> {
150 if self.len > 0 {
151 Some(&mut self.frames[0])
152 } else {
153 None
154 }
155 }
156
157 fn into_boxed_slice(self) -> Box<[Frame]> {
159 self.frames[..self.len].into()
160 }
161}
162
163#[cfg(feature = "alloc")]
166fn unwind_core(fp: usize, callback: impl FnMut(Frame) -> bool) {
167 unwind_core_with_max_depth(fp, max_depth(), callback);
168}
169
170#[cfg(feature = "alloc")]
171fn unwind_core_with_max_depth(
172 mut fp: usize,
173 max_depth: usize,
174 mut callback: impl FnMut(Frame) -> bool,
175) {
176 let Some(fp_range) = FP_RANGE.get() else {
177 log::error!("Backtrace not initialized. Call `axbacktrace::init` first.");
178 return;
179 };
180
181 let ip_range = IP_RANGE.get();
182 let mut depth = 0;
183
184 while fp_range.contains(&fp)
185 && depth < max_depth
186 && let Some(frame) = Frame::read(fp)
187 {
188 let next_fp = frame.fp;
194 if next_fp != 0 && next_fp <= fp {
197 break;
198 }
199
200 if let Some(ip_range) = ip_range
201 && !ip_range.contains(&frame.ip)
202 {
203 fp = next_fp;
204 depth += 1;
205 continue;
206 }
207
208 if !callback(frame) {
209 break;
210 }
211
212 if let Some(large_stack_end) = fp.checked_add(8 * 1024 * 1024)
213 && next_fp >= large_stack_end
214 {
215 break;
216 }
217
218 if next_fp == 0 {
219 break;
220 }
221
222 fp = next_fp;
223 depth += 1;
224 }
225}
226
227#[cfg(feature = "alloc")]
229pub fn unwind_stack(fp: usize) -> Vec<Frame> {
230 let mut frames = Vec::new();
231 unwind_core(fp, |frame| {
232 frames.push(frame);
233 true
234 });
235 frames
236}
237
238static MAX_DEPTH: AtomicUsize = AtomicUsize::new(32);
239
240pub fn set_max_depth(depth: usize) {
242 if depth > 0 {
243 MAX_DEPTH.store(depth, Ordering::Relaxed);
244 }
245}
246pub fn max_depth() -> usize {
248 MAX_DEPTH.load(Ordering::Relaxed)
249}
250
251pub const fn is_enabled() -> bool {
253 cfg!(feature = "alloc")
254}
255
256#[allow(dead_code)]
257#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
258enum Inner {
259 Unsupported,
260 Disabled,
261 #[cfg(feature = "alloc")]
262 Captured(Box<[Frame]>),
263}
264
265#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
272pub struct Backtrace {
273 inner: Inner,
274 kind: Option<&'static str>,
275}
276
277impl Backtrace {
278 pub fn capture() -> Self {
280 #[cfg(not(feature = "alloc"))]
281 return Self {
282 inner: Inner::Disabled,
283 kind: None,
284 };
285
286 #[cfg(feature = "alloc")]
287 {
288 use core::arch::asm;
289
290 let fp: usize;
291 cfg_if::cfg_if! {
292 if #[cfg(target_arch = "x86_64")] {
293 unsafe { asm!("mov {ptr}, rbp", ptr = out(reg) fp) };
294 } else if #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] {
295 unsafe { asm!("addi {ptr}, s0, 0", ptr = out(reg) fp) };
296 } else if #[cfg(target_arch = "aarch64")] {
297 unsafe { asm!("mov {ptr}, x29", ptr = out(reg) fp) };
298 } else if #[cfg(target_arch = "loongarch64")] {
299 unsafe { asm!("move {ptr}, $fp", ptr = out(reg) fp) };
300 } else {
301 return Self {
302 inner: Inner::Unsupported,
303 kind: None,
304 };
305 }
306 }
307
308 let mut buf = CaptureBuf::EMPTY;
309 unwind_core(fp, |frame| buf.push(frame));
310
311 core::hint::black_box(());
312
313 Self {
314 inner: Inner::Captured(buf.into_boxed_slice()),
315 kind: None,
316 }
317 }
318 }
319
320 #[allow(unused_variables)]
327 pub fn capture_trap(fp: usize, ip: usize, ra: usize) -> Self {
328 #[cfg(not(feature = "alloc"))]
329 return Self {
330 inner: Inner::Disabled,
331 kind: None,
332 };
333
334 #[cfg(feature = "alloc")]
335 {
336 let mut buf = CaptureBuf::EMPTY;
337 unwind_core(fp, |frame| buf.push(frame));
338
339 if let Some(first) = buf.first_mut()
346 && let Some(ip_range) = IP_RANGE.get()
347 && !ip_range.contains(&first.ip)
348 && ra != 0
349 && ip_range.contains(&ra)
350 {
351 first.ip = ra;
352 }
353
354 buf.insert_front(Frame {
355 fp,
356 ip: ip.wrapping_add(1),
357 });
358
359 Self {
360 inner: Inner::Captured(buf.into_boxed_slice()),
361 kind: None,
362 }
363 }
364 }
365
366 pub fn kind(mut self, kind: &'static str) -> Self {
368 self.kind = Some(kind);
369 self
370 }
371
372 #[cfg(feature = "dwarf")]
376 pub fn frames<'a>(&'a self) -> Option<FrameIter<'a>> {
377 let Inner::Captured(capture) = &self.inner else {
378 return None;
379 };
380
381 Some(FrameIter::new(capture))
382 }
383}
384
385impl Backtrace {
386 fn fmt_raw_block(&self, f: &mut fmt::Formatter<'_>, kind: &'static str) -> fmt::Result {
387 let arch = TARGET_ARCH;
388
389 writeln!(
390 f,
391 "BACKTRACE_BEGIN kind={} arch={} alloc={} dwarf={}",
392 kind,
393 arch,
394 cfg!(feature = "alloc"),
395 cfg!(feature = "dwarf")
396 )?;
397
398 match &self.inner {
399 Inner::Unsupported => {
400 writeln!(f, "BT_ERROR unsupported")?;
401 }
402 Inner::Disabled => {
403 if cfg!(feature = "alloc") {
404 writeln!(f, "BT_ERROR disabled")?;
405 } else {
406 writeln!(f, "BT_ERROR requires_alloc")?;
407 }
408 }
409 #[cfg(feature = "alloc")]
410 Inner::Captured(frames) => {
411 for (i, raw) in frames.iter().enumerate() {
412 writeln!(f, "BT {i} ip={:#x} fp={:#x}", raw.ip, raw.fp)?;
413 }
414 }
415 }
416
417 writeln!(f, "BACKTRACE_END")
418 }
419}
420
421impl fmt::Display for Backtrace {
422 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 if let Some(kind) = self.kind {
424 return self.fmt_raw_block(f, kind);
425 }
426
427 match &self.inner {
428 Inner::Unsupported => {
429 writeln!(f, "<unwinding unsupported>")
430 }
431 Inner::Disabled => {
432 if cfg!(feature = "alloc") {
433 writeln!(f, "<backtrace disabled>")
434 } else {
435 writeln!(f, "<backtrace requires alloc>")
436 }
437 }
438 #[cfg(feature = "alloc")]
439 Inner::Captured(frames) => {
440 writeln!(f, "Backtrace:")?;
441 #[cfg(feature = "dwarf")]
442 return dwarf::fmt_frames(f, frames);
443 #[cfg(not(feature = "dwarf"))]
444 {
445 for (i, raw) in frames.iter().enumerate() {
446 writeln!(f, "{i:>4}: {raw}")?;
447 }
448 Ok(())
449 }
450 }
451 }
452 }
453}
454
455impl fmt::Debug for Backtrace {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 fmt::Display::fmt(self, f)
458 }
459}
460
461#[cfg(all(test, feature = "alloc"))]
462mod tests {
463 use alloc::{boxed::Box, format, vec::Vec};
464
465 use super::*;
466
467 fn init_for_tests() {
468 init(0..usize::MAX, 0..usize::MAX);
469 }
470
471 fn boxed_frame_chain(ips: &[usize]) -> (Box<[Frame]>, usize) {
472 let mut frames = ips
473 .iter()
474 .map(|&ip| Frame { fp: 0, ip })
475 .collect::<Vec<_>>()
476 .into_boxed_slice();
477
478 let ptr = frames.as_mut_ptr();
479 for i in 0..frames.len() {
480 let next_fp = if i + 1 < frames.len() {
481 unsafe { ptr.add(i + 1) as usize }
482 } else {
483 0
484 };
485 frames[i].fp = next_fp;
486 }
487 (frames, ptr as usize)
488 }
489
490 #[test]
493 fn capture_buf_push_and_insert() {
494 let mut buf = CaptureBuf::EMPTY;
495 assert!(buf.push(Frame { fp: 1, ip: 0x10 }));
496 assert!(buf.push(Frame { fp: 2, ip: 0x20 }));
497 assert_eq!(buf.len, 2);
498
499 buf.insert_front(Frame { fp: 0, ip: 0x05 });
500 assert_eq!(buf.len, 3);
501 assert_eq!(
502 &*buf.clone().into_boxed_slice(),
503 &[
504 Frame { fp: 0, ip: 0x05 },
505 Frame { fp: 1, ip: 0x10 },
506 Frame { fp: 2, ip: 0x20 }
507 ]
508 );
509 }
510
511 #[test]
512 fn capture_buf_overflow_evicts_deepest() {
513 let mut buf = CaptureBuf::EMPTY;
514 for i in 0..CAPTURE_CAPACITY {
515 assert!(buf.push(Frame { fp: i, ip: i }));
516 }
517 assert!(!buf.push(Frame { fp: 0, ip: 0 })); buf.insert_front(Frame { fp: 99, ip: 0x99 });
519 assert_eq!(buf.len, CAPTURE_CAPACITY);
520 let boxed = buf.into_boxed_slice();
521 assert_eq!(boxed[0], Frame { fp: 99, ip: 0x99 });
522 assert_eq!(boxed.len(), CAPTURE_CAPACITY);
523 }
524
525 #[test]
526 fn into_boxed_slice_trims_to_len() {
527 let mut buf = CaptureBuf::EMPTY;
528 buf.push(Frame { fp: 1, ip: 0x10 });
529 buf.push(Frame { fp: 2, ip: 0x20 });
530 let boxed = buf.into_boxed_slice();
531 assert_eq!(boxed.len(), 2);
532 assert_eq!(boxed[0], Frame { fp: 1, ip: 0x10 });
533 }
534
535 #[test]
538 fn unwind_stack_collects_fake_frames() {
539 init_for_tests();
540 let (frames, start_fp) = boxed_frame_chain(&[0x1111, 0x2222, 0x3333]);
541 let out = unwind_stack(start_fp);
542 assert_eq!(out, frames.as_ref());
543 }
544
545 #[test]
546 fn unwind_core_callback_stop_early() {
547 init_for_tests();
548 let (_chain, start_fp) = boxed_frame_chain(&[0x1, 0x2, 0x3, 0x4, 0x5]);
549 let mut count = 0;
550 unwind_core(start_fp, |_| {
551 count += 1;
552 count < 3
553 });
554 assert_eq!(count, 3);
555 }
556
557 #[test]
558 fn unwind_stack_stops_on_non_advancing_frame_pointer() {
559 init_for_tests();
560 let mut frames = [Frame { fp: 0, ip: 0x1111 }, Frame { fp: 0, ip: 0x2222 }];
561 let base = frames.as_mut_ptr();
562 frames[0].fp = unsafe { base.add(1) as usize };
563 frames[1].fp = base as usize;
564
565 let out = unwind_stack(base as usize);
566 assert_eq!(out, [frames[0]]);
567 }
568
569 #[test]
570 fn frame_read_rejects_null_and_misaligned() {
571 assert!(Frame::read(0).is_none());
572 assert!(Frame::read(1).is_none());
573 assert!(Frame::read(3).is_none());
574 }
575
576 #[test]
579 fn capture_trap_ra_not_substituted_with_wide_range() {
580 init_for_tests();
581 let (_chain, start_fp) = boxed_frame_chain(&[0xDEAD]);
582 let bt = Backtrace::capture_trap(start_fp, 0x1000, 0xBEEF);
583 let Inner::Captured(frames) = &bt.inner else {
584 panic!("expected Captured")
585 };
586 assert_eq!(frames[0].ip, 0x1001);
587 assert_eq!(frames[1].ip, 0xDEAD); }
589
590 #[test]
595 fn stress_fill_buffer_exactly() {
596 init_for_tests();
597 let ips: Vec<usize> = (0..CAPTURE_CAPACITY).map(|i| 0xA000 + i).collect();
598 let (chain, start_fp) = boxed_frame_chain(&ips);
599 let out = unwind_stack(start_fp);
600 assert_eq!(out.len(), CAPTURE_CAPACITY);
601 assert_eq!(out.as_slice(), chain.as_ref());
602 }
603
604 #[test]
607 fn stress_trap_near_capacity() {
608 init_for_tests();
609 let n = CAPTURE_CAPACITY - 1;
610 let ips: Vec<usize> = (0..n).map(|i| 0xB000 + i).collect();
611 let (_chain, start_fp) = boxed_frame_chain(&ips);
612
613 let bt = Backtrace::capture_trap(start_fp, 0xC000, 0);
614 let Inner::Captured(frames) = &bt.inner else {
615 panic!("expected Captured")
616 };
617 assert_eq!(frames.len(), CAPTURE_CAPACITY);
618 assert_eq!(frames[0].ip, 0xC001);
620 for (i, f) in frames[1..].iter().enumerate() {
622 assert_eq!(f.ip, 0xB000 + i);
623 }
624 }
625
626 #[test]
629 fn stress_trap_overflow_evicts_deepest() {
630 init_for_tests();
631 let ips: Vec<usize> = (0..CAPTURE_CAPACITY).map(|i| 0xD000 + i).collect();
632 let (_chain, start_fp) = boxed_frame_chain(&ips);
633
634 let bt = Backtrace::capture_trap(start_fp, 0xE000, 0);
635 let Inner::Captured(frames) = &bt.inner else {
636 panic!("expected Captured")
637 };
638 assert_eq!(frames.len(), CAPTURE_CAPACITY);
639 assert_eq!(frames[0].ip, 0xE001);
641 for (i, f) in frames[1..].iter().enumerate() {
643 assert_eq!(f.ip, 0xD000 + i);
644 }
645 }
647
648 #[test]
650 fn stress_deep_chain_truncation() {
651 init_for_tests();
652 let ips: Vec<usize> = (0..64).map(|i| 0xF000 + i).collect();
653 let (chain, start_fp) = boxed_frame_chain(&ips);
654
655 let mut out = Vec::new();
656 unwind_core_with_max_depth(start_fp, 16, |frame| {
657 out.push(frame);
658 true
659 });
660 assert_eq!(out.len(), 16);
661 assert_eq!(out.as_slice(), &chain[..16]);
663 }
664
665 #[test]
667 fn stress_repeated_create_drop() {
668 init_for_tests();
669 let (chain, start_fp) = boxed_frame_chain(&[0x100, 0x200, 0x300]);
670 for _ in 0..500 {
671 let bt = Backtrace::capture_trap(start_fp, 0x400, 0);
672 let Inner::Captured(frames) = &bt.inner else {
673 panic!("expected Captured")
674 };
675 assert!(frames.len() >= 3);
676 drop(bt);
677 }
678 let _ = &chain;
680 }
681
682 #[test]
684 fn stress_interleaved_capture_format() {
685 init_for_tests();
686 let (chain, start_fp) = boxed_frame_chain(&[0x500, 0x600]);
687
688 for i in 0..100 {
689 let bt = Backtrace::capture_trap(start_fp, 0x700, 0);
690 let s = format!("{bt}");
691 assert!(
693 s.contains("0x701"),
694 "iteration {i}: missing trap IP in output"
695 );
696
697 let bt_human = Backtrace::capture_trap(start_fp, 0x700, 0);
699 let human = format!("{bt_human}");
700 assert!(!human.is_empty(), "iteration {i}: empty human output");
701
702 drop(bt);
703 drop(bt_human);
704 }
705 let _ = &chain;
706 }
707
708 #[test]
710 fn stress_repeated_clone() {
711 init_for_tests();
712 let (chain, start_fp) = boxed_frame_chain(&[0x800, 0x900, 0xA00]);
713 let original = Backtrace::capture_trap(start_fp, 0xB00, 0);
714
715 for _ in 0..200 {
716 let cloned = original.clone();
717 assert_eq!(cloned, original);
718 }
719 let _ = &chain;
720 }
721
722 #[test]
724 fn stress_size_stability() {
725 assert_eq!(
727 core::mem::size_of::<Frame>(),
728 2 * core::mem::size_of::<usize>()
729 );
730 assert_eq!(
731 core::mem::align_of::<Frame>(),
732 core::mem::align_of::<usize>()
733 );
734
735 let bt_size = core::mem::size_of::<Backtrace>();
738 assert!(
739 bt_size > 0 && bt_size <= 48,
740 "Backtrace size unexpected: {bt_size}"
741 );
742
743 let cap_size = core::mem::size_of::<CaptureBuf>();
745 let expected =
746 CAPTURE_CAPACITY * core::mem::size_of::<Frame>() + core::mem::size_of::<usize>();
747 assert_eq!(cap_size, expected, "CaptureBuf size mismatch");
748 }
749
750 #[test]
752 fn stress_frame_alignment() {
753 let align = core::mem::align_of::<Frame>();
754 assert!(align > 0);
755 assert!(align.is_power_of_two());
756
757 for offset in 1..align {
759 assert!(
760 Frame::read(offset).is_none(),
761 "misaligned {offset} should fail"
762 );
763 }
764 assert!(Frame::read(0).is_none());
766 }
767}