Panic!
Now that our panic functions are growing, it’s time to create a dedicated panic.rs
instead of leaving everything scattered in lib.rs. We also need to properly display
registers on kernel panic, then clear them before halting — leaving no sensitive data
lingering in CPU registers.
Capturing the Registers
When the kernel panics via Rust’s panic!() macro, there’s no InterruptFrame being
passed in — unlike exception handlers which receive one from the assembly stubs. So we
need to capture the current register state ourselves.
The trick is pusha — we let the CPU push all registers onto the stack, then copy them
directly into an InterruptFrame struct using rep movsd (repeat move string doubleword),
which copies 8 x 4-byte values in one shot:
#![allow(unused)]
fn main() {
pub fn capture_registers() -> InterruptFrame {
let mut frame = InterruptFrame::new();
unsafe {
core::arch::asm!(
"pusha",
"mov esi, esp",
"mov edi, {0}",
"mov ecx, 8",
"rep movsd",
"popa",
in(reg) &mut frame as *mut InterruptFrame as u32,
out("edi") _,
out("ecx") _,
);
}
frame
}
}
pusha dumps all general purpose registers onto the stack. esp then points directly
to them, so we use it as the source (esi = esp) and copy 8 dwords into our frame.
popa restores everything afterward so execution continues normally.
Note: EIP, CS and EFLAGS are left as zero — EIP isn’t directly readable as a
general purpose register, and the others aren’t captured by pusha. For a software
panic this is acceptable; hardware exceptions get the real values via the CPU’s automatic
stack push.
We tried capturing registers one at a time with individual asm! blocks first, but
i386 simply doesn’t have enough free registers to hold all the output values simultaneously
— the compiler uses the same registers as scratch and corrupts the values. The pusha
approach sidesteps this entirely.
Cleaning the Registers
After displaying the register state, we clear all general purpose registers using XOR
operations. XORing a register with itself is the fastest way to zero it and leaves no
sensitive data behind:
#![allow(unused)]
fn main() {
pub fn clear_registers() {
unsafe {
core::arch::asm!(
"xor eax, eax",
"xor ebx, ebx",
"xor ecx, ecx",
"xor edx, edx",
"xor esi, esi",
"xor edi, edi",
"xor ebp, ebp",
options(nomem, nostack)
);
}
}
}
Note: we don’t zero ESP — that’s the stack pointer. Zeroing it would instantly corrupt
the stack and cause a triple fault before we even reach the loop {}.
Updating kernel_panic
With capture and clear available, kernel_panic becomes a proper crash handler:
#![allow(unused)]
fn main() {
pub fn kernel_panic(msg: &str, frame: &InterruptFrame) -> ! {
writer().printc("\n\n!!! KERNEL PANIC !!!", Color::White, Color::Red);
println!("\nError: {}", msg);
println!("Register state at panic:");
frame.print();
clear_registers();
loop {}
}
}
The #[panic_handler] hooks into this by calling capture_registers() first:
#![allow(unused)]
fn main() {
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
let frame = capture_registers();
kernel_panic(info.message().as_str().unwrap_or("unknown panic"), &frame);
}
}
This means every Rust panic!() now automatically captures and displays the register
state — no manual frame passing needed at the call site.
kernel_warn
For non-fatal errors, kernel_warn just displays a warning and returns — no register
dump, no halt:
#![allow(unused)]
fn main() {
pub fn kernel_warn(msg: &str) {
writer().printc("\n\n!!! KERNEL WARNING !!!", Color::Black, Color::Yellow);
println!("\nError: {}", msg);
}
}
Testing
Added sys panic to NPS to test both paths:
> sys panic -w
!!! KERNEL WARN !!!
Error: Test
> sys panic -p
!!! KERNEL PANIC !!!
Error: Test
Register state at panic:
EAX: 0x0025AD7C EBX: 0x0024AA1C
ECX: 0x0025AD7C EDX: 0x00000001
ESI: 0x00000000 EDI: 0x00001000
EBP: 0x00000000 ESP: 0x0025AD64
EIP: 0x00000000 EFL: 0x00000000
The warn returns to the shell. The panic halts the kernel.