diff --git a/Cargo.lock b/Cargo.lock
index 97a36a049555b5c08e16b174205f107399b3ff79..21525f45a2cbe106ccc63278fe813c999ad38ac8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -210,6 +210,12 @@ version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
 
+[[package]]
+name = "elf"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2b183d6ce6ca4cf30e3db37abf5b52568b5f9015c97d9fbdd7026aa5dcdd758"
+
 [[package]]
 name = "exr"
 version = "1.6.3"
@@ -317,6 +323,7 @@ version = "0.1.0"
 dependencies = [
  "bit_field",
  "chrono",
+ "elf",
  "image",
  "limine",
  "paste",
diff --git a/Cargo.toml b/Cargo.toml
index fbf5ebe817768389dae87b4b24163c74227fd5f5..d12d3e2b4851e0979cee1f857e0e046d63c9a077 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,6 +21,7 @@ uart_16550 = "0.2"
 pc-keyboard = "0.7"
 paste = "1.0"
 bit_field = "0.10"
+elf = { version = "0.7", default-features = false }
 # bitvec = "1.0.1"
 #tracing = { version = "0.1.37", default-features = false }
 
diff --git a/src/arch/x86_64/mod.rs b/src/arch/x86_64/mod.rs
index 4f474d49c10b21cc65df9ca0b7a6a5cc7126cb1a..aa63e3d102a223d6266ea0d596a0afe314a75682 100644
--- a/src/arch/x86_64/mod.rs
+++ b/src/arch/x86_64/mod.rs
@@ -1,5 +1,5 @@
 use crate::{driver, error, smp::Cpu, warn};
-use x86_64::instructions::{self as ins, interrupts as int, random::RdRand};
+use x86_64::instructions::{self as ins, random::RdRand};
 
 //
 
@@ -39,14 +39,10 @@ pub fn early_per_cpu(cpu: &Cpu) {
 
     if cfg!(debug_assertions) {
         warn!("[debug_assertions] {cpu} throwing a debug interrupt exception");
-        debug_interrupt();
+        int::debug();
     }
 }
 
-pub fn debug_interrupt() {
-    int::int3();
-}
-
 pub fn rng_seed() -> u64 {
     RdRand::new().and_then(RdRand::get_u64).unwrap_or_else(|| {
         error!("Failed to generate a rng seed with x86_64 RDSEED");
@@ -54,12 +50,36 @@ pub fn rng_seed() -> u64 {
     })
 }
 
-pub fn wait_interrupt() {
-    ins::hlt()
+pub mod int {
+    use x86_64::instructions::interrupts as int;
+
+    pub fn debug() {
+        int::int3();
+    }
+
+    pub fn disable() {
+        int::disable()
+    }
+
+    pub fn enable() {
+        int::enable()
+    }
+
+    pub fn are_enabled() -> bool {
+        int::are_enabled()
+    }
+
+    pub fn without<T>(f: impl FnOnce() -> T) -> T {
+        int::without_interrupts(f)
+    }
+
+    pub fn wait() {
+        int::enable_and_hlt()
+    }
 }
 
 pub fn done() -> ! {
     loop {
-        wait_interrupt()
+        int::wait()
     }
 }
diff --git a/src/backtrace.rs b/src/backtrace.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a0ca0ab01fa2a7dc2592d4901cfaf0ef213a4346
--- /dev/null
+++ b/src/backtrace.rs
@@ -0,0 +1,109 @@
+use core::{
+    arch::asm,
+    mem,
+    ptr::{self, NonNull},
+};
+
+use crate::{arch, boot, println};
+use elf::{
+    endian::AnyEndian, parse::ParsingTable, string_table::StringTable, symbol::SymbolTable,
+    ElfBytes, ParseError,
+};
+use spin::Lazy;
+
+//
+
+pub type BacktraceResult<T> = Result<T, BacktraceError>;
+
+#[derive(Debug)]
+pub enum BacktraceError {
+    NoSymtabOrStrtab,
+    ElfNotLoaded,
+    ElfParse(ParseError),
+
+    // TODO: this is temporary
+    Inner(&'static Self),
+}
+
+static KERNEL_ELF: Lazy<BacktraceResult<ElfBytes<'static, AnyEndian>>> = Lazy::new(|| {
+    let bytes = boot::kernel_file().ok_or(BacktraceError::ElfNotLoaded)?;
+    ElfBytes::minimal_parse(bytes).map_err(BacktraceError::ElfParse)
+    // ElfBytes::minimal_parse(bytes)
+    //     .ok()
+    //     .ok_or(BacktraceError::ElfParse)
+});
+
+static SYMTAB: Lazy<
+    Result<(SymbolTable<'static, AnyEndian>, StringTable<'static>), BacktraceError>,
+> = Lazy::new(|| {
+    let elf = KERNEL_ELF.as_ref().map_err(BacktraceError::Inner)?;
+
+    elf.symbol_table()
+        .map_err(BacktraceError::ElfParse)?
+        .ok_or(BacktraceError::NoSymtabOrStrtab)
+});
+
+static UNKNOWN: &str = "<unknown>";
+
+pub fn symbol(instr_ptr: u64) -> Result<&'static str, BacktraceError> {
+    let (symtab, strtab) = SYMTAB.as_ref().map_err(BacktraceError::Inner)?;
+
+    let symbol = symtab
+        .iter()
+        .find(|sym| (sym.st_value..sym.st_value + sym.st_size).contains(&instr_ptr));
+
+    let Some(symbol) = symbol else {
+        return Ok(UNKNOWN);
+    };
+
+    strtab
+        .get(symbol.st_name as _)
+        .map_err(BacktraceError::ElfParse)
+}
+
+pub fn unwind_stack(mut f: impl FnMut(usize, &'static str)) {
+    arch::int::disable();
+
+    // TODO: move to arch
+    let mut frame_ptr: usize;
+    let mut instr_ptr: usize = x86_64::registers::read_rip().as_u64() as _;
+    unsafe {
+        asm!("mov {}, rbp", out(reg) frame_ptr);
+    }
+
+    println!("{frame_ptr} {instr_ptr}");
+
+    if frame_ptr == 0 {
+        println!("empty");
+    }
+
+    loop {
+        if frame_ptr == 0 {
+            break;
+        }
+
+        let rip_rbp = frame_ptr + mem::size_of::<usize>();
+
+        let instr_ptr = unsafe { ptr::read_volatile(rip_rbp as *const usize) };
+        if instr_ptr == 0 {
+            break;
+        }
+
+        frame_ptr = unsafe { ptr::read_volatile(frame_ptr as *const usize) };
+
+        f(instr_ptr, symbol(instr_ptr as _).unwrap_or(UNKNOWN));
+    }
+
+    // TODO: should reset to what it was before
+    arch::int::enable();
+}
+
+pub fn print_backtrace() {
+    println!("--[ begin backtrace ]--");
+    let mut i = 0usize;
+    unwind_stack(|ip, sym| {
+        println!("{i:>3} : {ip:#018x} - {sym}");
+        i += 1;
+    });
+    println!("--[ end backtrace ]--");
+}
diff --git a/src/boot/limine/cmdline.rs b/src/boot/limine/cmdline.rs
index 9d92ae6900e4850953f03b09e90d42c722b1ef42..0692c2189fdc0d22cc70e2f172e7cc5a1901f1d7 100644
--- a/src/boot/limine/cmdline.rs
+++ b/src/boot/limine/cmdline.rs
@@ -1,9 +1,8 @@
-use limine::LimineKernelFileRequest;
+use super::kernel::REQ;
 
 //
 
 pub fn cmdline() -> Option<&'static str> {
-    static REQ: LimineKernelFileRequest = LimineKernelFileRequest::new(0);
     REQ.get_response()
         .get()
         .and_then(|resp| resp.kernel_file.get())
diff --git a/src/boot/limine/kernel.rs b/src/boot/limine/kernel.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1c7085b081644cd488381e720b6a44c8d7c3bb3d
--- /dev/null
+++ b/src/boot/limine/kernel.rs
@@ -0,0 +1,15 @@
+use core::slice;
+use limine::LimineKernelFileRequest;
+
+//
+
+pub(crate) static REQ: LimineKernelFileRequest = LimineKernelFileRequest::new(0);
+
+pub fn kernel_file() -> Option<&'static [u8]> {
+    REQ.get_response()
+        .get()
+        .and_then(|resp| resp.kernel_file.get())
+        .and_then(|file| {
+            Some(unsafe { slice::from_raw_parts(file.base.as_ptr()?, file.length as _) })
+        })
+}
diff --git a/src/boot/limine/mod.rs b/src/boot/limine/mod.rs
index 54c9487af87a7a08e7b6908894faca5fb7fac82c..ff232a0c01cc02c34c87f39fe929961f9c54e592 100644
--- a/src/boot/limine/mod.rs
+++ b/src/boot/limine/mod.rs
@@ -8,6 +8,7 @@ pub use addr::phys_addr;
 pub use addr::virt_addr;
 pub use cmdline::cmdline;
 pub use framebuffer::framebuffer;
+pub use kernel::kernel_file;
 pub use mem::memmap;
 pub use rsdp::rsdp;
 pub use smp::{boot_cpu, init as smp_init};
@@ -18,6 +19,7 @@ pub use term::_print;
 mod addr;
 mod cmdline;
 mod framebuffer;
+mod kernel;
 mod mem;
 mod rsdp;
 mod smp;
diff --git a/src/main.rs b/src/main.rs
index 9d8d9b1cb4595b0b7e73c0e46ec57eca72c0a747..8121468e0480525b02a37603fc9a657602d24d73 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,17 +3,19 @@
 #![no_std]
 #![no_main]
 //
-#![feature(format_args_nl)]
-#![feature(abi_x86_interrupt)]
-#![feature(allocator_api)]
-#![feature(pointer_is_aligned)]
-#![feature(int_roundings)]
-#![feature(array_chunks)]
-#![feature(cfg_target_has_atomic)]
-#![feature(slice_as_chunks)]
-#![feature(core_intrinsics)]
-//
-#![feature(custom_test_frameworks)]
+#![feature(
+    format_args_nl,
+    abi_x86_interrupt,
+    allocator_api,
+    pointer_is_aligned,
+    int_roundings,
+    array_chunks,
+    cfg_target_has_atomic,
+    slice_as_chunks,
+    core_intrinsics,
+    custom_test_frameworks,
+    panic_can_unwind
+)]
 #![test_runner(crate::testfw::test_runner)]
 #![reexport_test_harness_main = "test_main"]
 
@@ -27,6 +29,7 @@ extern crate alloc;
 
 #[path = "arch/x86_64/mod.rs"]
 pub mod arch;
+pub mod backtrace;
 pub mod boot;
 pub mod driver;
 pub mod log;
@@ -70,6 +73,9 @@ fn kernel_main() -> ! {
     // ofc. every kernel has to have this cringy ascii name splash
     info!("\n{}\n", include_str!("./splash"));
 
+    backtrace::print_backtrace();
+    panic!("test panic");
+
     if let Some(bl) = boot::BOOT_NAME.get() {
         debug!("{KERNEL_NAME} {KERNEL_VERSION} was booted with {bl}");
     }
diff --git a/src/panic.rs b/src/panic.rs
index a06b3c7f917c25310131879cbdc6baa5f92a3670..914dae05ce31a8af7b2983b0a8b241c29a40bb19 100644
--- a/src/panic.rs
+++ b/src/panic.rs
@@ -1,4 +1,7 @@
-use crate::arch::done;
+use crate::{
+    arch::{done, int},
+    backtrace,
+};
 use core::panic::PanicInfo;
 
 //
@@ -6,13 +9,21 @@ use core::panic::PanicInfo;
 #[cfg(not(test))]
 #[panic_handler]
 fn panic_handler(info: &PanicInfo) -> ! {
-    crate::println!("Kernel CPU {info}");
+    int::disable();
+    panic_unwind(info);
     done();
 }
 
 #[cfg(test)]
 #[panic_handler]
 fn panic_handler(info: &PanicInfo) -> ! {
+    int::disable();
+    panic_unwind(info);
     crate::testfw::test_panic_handler(info);
     done();
 }
+
+fn panic_unwind(info: &PanicInfo) {
+    crate::println!("Kernel CPU {info} {}", info.can_unwind());
+    backtrace::print_backtrace();
+}