diff --git a/.cargo/config.toml b/.cargo/config.toml index a5ed15ea..3d878824 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,6 @@ [build] target = "aarch64-unknown-none-softfloat" +rustflags = ["-C", "force-frame-pointers=yes"] [target.aarch64-unknown-none-softfloat] runner = "scripts/qemu_runner.py" diff --git a/libkernel/src/arch/arm64/mod.rs b/libkernel/src/arch/arm64/mod.rs index 861afd92..9beac615 100644 --- a/libkernel/src/arch/arm64/mod.rs +++ b/libkernel/src/arch/arm64/mod.rs @@ -1,3 +1,4 @@ //! AArch64 (ARM64) specific implementations. pub mod memory; +pub mod stacktrace; diff --git a/libkernel/src/arch/arm64/stacktrace.rs b/libkernel/src/arch/arm64/stacktrace.rs new file mode 100644 index 00000000..ea053a93 --- /dev/null +++ b/libkernel/src/arch/arm64/stacktrace.rs @@ -0,0 +1,46 @@ +//! Aarch64 stack trace implementation + +use crate::StackTrace; +use core::arch::asm; + +/// Aarch64 stack trace implementation +pub struct StackTraceImpl { + /// Frame pointer + pub fp: usize, + /// PC ptr + pub pc_ptr: *const usize, +} + +impl StackTrace for StackTraceImpl { + #[inline(always)] + unsafe fn start() -> Option { + unsafe { + let fp: usize; + asm!("mov {}, fp", out(reg) fp); + let pc_ptr = fp.checked_add(size_of::())?; + Some(Self { + fp, + pc_ptr: pc_ptr as *const usize, + }) + } + } + + unsafe fn next(self) -> Option { + unsafe { + let fp = *(self.fp as *const usize); + let pc_ptr = fp.checked_add(size_of::())?; + Some(Self { + fp, + pc_ptr: pc_ptr as *const usize, + }) + } + } + + fn fp(&self) -> usize { + self.fp + } + + fn pc_ptr(&self) -> *const usize { + self.pc_ptr + } +} diff --git a/libkernel/src/lib.rs b/libkernel/src/lib.rs index 9979254d..d5ff32c4 100644 --- a/libkernel/src/lib.rs +++ b/libkernel/src/lib.rs @@ -112,6 +112,30 @@ pub trait CpuOps: 'static { fn enable_interrupts(); } +/// Generic stack trace methods +pub trait StackTrace: Sized { + /// Start a stack trace, if supported. + /// + /// # Safety + /// Will mess up stack + unsafe fn start() -> Option { + None + } + + /// Continue to next frame in stack, returning `None` if there are no more frames or if the next frame is invalid. + /// + /// # Safety + /// Will mess up stack + unsafe fn next(self) -> Option { + None + } + + /// Frame pointer + fn fp(&self) -> usize; + /// PC PTR + fn pc_ptr(&self) -> *const usize; +} + #[cfg(test)] #[allow(missing_docs)] pub mod test { diff --git a/scripts/qemu_runner.py b/scripts/qemu_runner.py index e519f125..a0b8528a 100755 --- a/scripts/qemu_runner.py +++ b/scripts/qemu_runner.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 import argparse +import re +import shutil import subprocess +import sys parser = argparse.ArgumentParser(description="QEMU runner") @@ -14,6 +17,7 @@ parser.add_argument("--memory", default="2G") parser.add_argument("--debug", action="store_true", help="Enable QEMU debugging") parser.add_argument("--display", action="store_true", help="Add a display device to the VM") +parser.add_argument("--demangle", action="store_true", help="Add a demangled backtrace") @@ -68,4 +72,59 @@ qemu_command += extra_args -subprocess.run(qemu_command, check=True) +if args.demangle: + addr2line = ( + shutil.which("aarch64-none-elf-addr2line") + or shutil.which("llvm-addr2line") + or shutil.which("addr2line") + ) + pc_pattern = re.compile(r"\bPC ([0-9a-fA-F]+)\b") + decode_cache = {} + + + def decode_pc(pc: str) -> str | None: + if addr2line is None: + return None + + pc = pc.lower() + if pc in decode_cache: + return decode_cache[pc] + + result = subprocess.run( + [addr2line, "-e", elf_executable, "-f", "-C", "-p", f"0x{pc}"], + check=False, + capture_output=True, + text=True, + ) + + decoded = result.stdout.strip() or None + decode_cache[pc] = decoded + return decoded + + + with subprocess.Popen( + qemu_command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) as proc: + assert proc.stdout is not None + + for line in proc.stdout: + sys.stdout.write(line) + sys.stdout.flush() + + match = pc_pattern.search(line) + if match is None: + continue + + decoded = decode_pc(match.group(1)) + if decoded is not None: + print(f"\t=> {decoded}", flush=True) + + ret = proc.wait() + if ret != 0: + raise subprocess.CalledProcessError(ret, qemu_command) +else: + subprocess.run(qemu_command, check=True) diff --git a/src/arch/arm64/mod.rs b/src/arch/arm64/mod.rs index 6711c661..9b69f67c 100644 --- a/src/arch/arm64/mod.rs +++ b/src/arch/arm64/mod.rs @@ -8,7 +8,7 @@ use cpu_ops::{local_irq_restore, local_irq_save}; use exceptions::ExceptionState; use libkernel::{ CpuOps, - arch::arm64::memory::pg_tables::L0Table, + arch::arm64::{memory::pg_tables::L0Table, stacktrace::StackTraceImpl}, error::Result, memory::{ address::{UA, VA}, @@ -45,6 +45,10 @@ mod proc; pub mod psci; pub mod ptrace; +pub(crate) use self::boot::memory::{KERNEL_STACK_AREA, KERNEL_STACK_PG_ORDER}; +pub(crate) use self::exceptions::EMERG_STACK_END; +pub(crate) use self::memory::IMAGE_BASE; + pub struct Aarch64 {} impl CpuOps for Aarch64 { @@ -86,6 +90,7 @@ impl VirtualMemory for Aarch64 { impl Arch for Aarch64 { type UserContext = ExceptionState; type PTraceGpRegs = Arm64PtraceGPRegs; + type ArchStackTrace = StackTraceImpl; const PAGE_OFFSET: usize = PAGE_OFFSET; diff --git a/src/arch/mod.rs b/src/arch/mod.rs index 750e3df6..f925e75e 100644 --- a/src/arch/mod.rs +++ b/src/arch/mod.rs @@ -20,7 +20,7 @@ use crate::{ use alloc::string::String; use alloc::sync::Arc; use libkernel::{ - CpuOps, + CpuOps, StackTrace, error::Result, memory::{ address::{UA, VA}, @@ -37,6 +37,8 @@ pub trait Arch: CpuOps + VirtualMemory { /// The type for GP regs copied via `PTRACE_GETREGSET`. type PTraceGpRegs: UserCopyable + for<'a> From<&'a Self::UserContext>; + type ArchStackTrace: StackTrace; + /// The starting address for the logical mapping of all physical ram. const PAGE_OFFSET: usize; @@ -205,6 +207,10 @@ pub trait Arch: CpuOps + VirtualMemory { dst: *mut u8, len: usize, ) -> impl Future>; + + unsafe fn stack_trace() -> Option { + unsafe { Self::ArchStackTrace::start() } + } } #[cfg(target_arch = "aarch64")] @@ -212,3 +218,7 @@ mod arm64; #[cfg(target_arch = "aarch64")] pub use self::arm64::Aarch64 as ArchImpl; +#[cfg(target_arch = "aarch64")] +pub(crate) use self::arm64::{ + EMERG_STACK_END, IMAGE_BASE, KERNEL_STACK_AREA, KERNEL_STACK_PG_ORDER, +}; diff --git a/src/backtrace/mod.rs b/src/backtrace/mod.rs new file mode 100644 index 00000000..ede75e49 --- /dev/null +++ b/src/backtrace/mod.rs @@ -0,0 +1,104 @@ +use crate::arch::{ + Arch, ArchImpl, EMERG_STACK_END, IMAGE_BASE, KERNEL_STACK_AREA, KERNEL_STACK_PG_ORDER, +}; +use core::slice; +use libkernel::StackTrace; +use libkernel::memory::{PAGE_SIZE, address::VA, region::VirtMemoryRegion}; +use log::error; +#[cfg(target_pointer_width = "32")] +use object::elf::FileHeader32 as FileHeader; +#[cfg(target_pointer_width = "64")] +use object::elf::FileHeader64 as FileHeader; +use object::{ + NativeEndian, elf, + read::elf::{FileHeader as _, Sym as _}, +}; + +const MAX_FRAMES: usize = 64; + +fn emergency_stack_area() -> VirtMemoryRegion { + let stack_size = PAGE_SIZE << KERNEL_STACK_PG_ORDER; + VirtMemoryRegion::new(EMERG_STACK_END.sub_bytes(stack_size), stack_size) +} + +fn is_valid_frame_pointer(frame: &T) -> bool { + let fp_virt = VA::from_value(frame.fp()); + let pc_virt = VA::from_value(frame.pc_ptr().addr()); + let emergency_stack = emergency_stack_area(); + + let in_kernel_stack = + |va| KERNEL_STACK_AREA.contains_address(va) || emergency_stack.contains_address(va); + + in_kernel_stack(fp_virt) + && in_kernel_stack(pc_virt) + && (frame.fp() as *const usize).is_aligned() + && frame.pc_ptr().is_aligned() +} + +pub fn print_backtrace() { + unsafe { + let kernel_ptr = IMAGE_BASE.cast::().as_ptr(); + let elf_header: &FileHeader = object::pod::from_bytes(slice::from_raw_parts( + kernel_ptr, + size_of::>(), + )) + .unwrap() + .0; + + // This assumes that the linker places .shstrtab as last section. If it + // isn't, that just causes a recursive panic, not UB. + let kernel_size = elf_header.e_shoff(NativeEndian) as usize + + usize::from(elf_header.e_shnum(NativeEndian)) + * usize::from(elf_header.e_shentsize(NativeEndian)); + let kernel_slice = slice::from_raw_parts(kernel_ptr, kernel_size); + + let symbols = elf_header + .sections(NativeEndian, kernel_slice) + .unwrap() + .symbols(NativeEndian, kernel_slice, elf::SHT_SYMTAB) + .unwrap(); + + let mut frame = ArchImpl::stack_trace(); + + for _ in 0..MAX_FRAMES { + let Some(frame_) = frame else { + break; + }; + + if !is_valid_frame_pointer(&frame_) { + error!(" {:>016x}: INVALID FRAME", frame_.fp); + break; + } + + let pc = *frame_.pc_ptr; + if pc == 0 { + error!(" {:>016x}: EMPTY RETURN", frame_.fp); + break; + } + + error!(" FP {:>016x}: PC {:>016x}", frame_.fp, pc); + + for sym in symbols.iter() { + if sym.st_type() != elf::STT_FUNC { + continue; + } + let sym_addr = sym.st_value.get(NativeEndian) as usize; + if !(pc >= sym_addr && pc < sym_addr + sym.st_size.get(NativeEndian) as usize) { + continue; + } + + let sym_offset = pc - sym_addr; + if let Some(sym_name) = sym + .name(NativeEndian, symbols.strings()) + .ok() + .and_then(|name| core::str::from_utf8(name).ok()) + { + error!(" {sym_name} @ {sym_addr:>016X}+{sym_offset:>04X}"); + } else { + error!(" {sym_addr:>016X}+{sym_offset:>04X}"); + } + } + frame = frame_.next(); + } + } +} diff --git a/src/main.rs b/src/main.rs index b41c707d..4cd42bd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,7 @@ extern crate alloc; extern crate moss_macros; mod arch; +mod backtrace; mod clock; mod console; mod drivers; @@ -74,6 +75,8 @@ fn on_panic(info: &PanicInfo) -> ! { error!("Kernel panicked at unknown location: {panic_msg}"); } + backtrace::print_backtrace(); + ArchImpl::power_off(); }