Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Interrupt Frame

When an interrupt fires, the CPU is in the middle of executing something. To handle the interrupt properly — and especially to produce useful panic output or perform context switching later — we need to capture a snapshot of the CPU state at that exact moment.

This snapshot is the InterruptFrame.

What Gets Captured

When an interrupt fires, the CPU automatically pushes three things onto the stack:

eflags   ← pushed first (highest address)
cs       ← code segment
eip      ← instruction pointer (where the fault occurred)

Then our assembly wrapper calls pusha, which pushes all general purpose registers in this order:

eax, ecx, edx, ebx, esp, ebp, esi, edi

So after pusha, the stack contains a complete snapshot of the CPU state. We represent this in Rust as:

#![allow(unused)]
fn main() {
#[repr(C)]
pub struct InterruptFrame {
    // pushed by pusha (in reverse order, edi at top)
    pub edi: u32,
    pub esi: u32,
    pub ebp: u32,
    pub esp: u32,
    pub ebx: u32,
    pub edx: u32,
    pub ecx: u32,
    pub eax: u32,
    // pushed by CPU automatically
    pub eip: u32,
    pub cs: u32,
    pub eflags: u32,
}
}

#[repr(C)] is critical here — it tells Rust to lay out the struct fields in the exact order they’re declared, matching what’s on the stack. Without it, Rust might reorder fields and the values would be garbage.

Passing the Frame to Rust

After pusha, esp points directly to the saved register block — that’s exactly our InterruptFrame. We pass a pointer to it by pushing esp before calling the dispatcher:

divide_by_zero_handler:
    cli
    pusha
    push esp        ; pointer to InterruptFrame (2nd arg)
    push 0          ; interrupt number (1st arg)
    call rust_interrupt_dispatcher
    add esp, 8      ; clean up both pushed arguments
    popa
    iretd

The add esp, 8 after the call cleans up the two arguments we pushed (4 bytes each). Then popa restores all the registers and iretd returns from the interrupt.

Printing the Frame

The InterruptFrame has a print() method that dumps all registers in a readable format:

#![allow(unused)]
fn main() {
pub fn print(&self) {
    println!("  EAX: 0x{:08X}  EBX: 0x{:08X}", self.eax, self.ebx);
    println!("  ECX: 0x{:08X}  EDX: 0x{:08X}", self.ecx, self.edx);
    println!("  ESI: 0x{:08X}  EDI: 0x{:08X}", self.esi, self.edi);
    println!("  EBP: 0x{:08X}  ESP: 0x{:08X}", self.ebp, self.esp);
    println!("  EIP: 0x{:08X}  EFL: 0x{:08X}", self.eip, self.eflags);
}
}

EIP is the most useful field in a crash — it tells you exactly which instruction was executing when things went wrong. Combined with a disassembly of the kernel binary, you can pinpoint the exact line of code that caused the fault.

Why This Matters

The InterruptFrame is the foundation for:

  • Panic output — print register state when something goes wrong
  • Stack saving — preserve the full CPU state before halting
  • Context switching — save and restore process state (KFS_5+)

Without it, a kernel panic just says “something went wrong” with no way to know where or why.