Low-level ISR stubs are defined in arch/isr.asm and linked into the kernel. They perform minimal setup before calling Rust handlers:
kernel/src/arch/isr.asm
extern isr_page_faultextern crash_frameglobal isr_14isr_14: ; Page Fault (#PF) cli ; Save error code pop rax ; Save all registers to crash_frame mov [crash_frame + 32], rax ; RAX mov [crash_frame + 40], rbx ; RBX mov [crash_frame + 48], rcx ; RCX ; ... (all other GPRs) ... ; Save RIP, RSP, RFLAGS from interrupt stack pop qword [crash_frame + 0] ; RIP pop rax ; CS (discard) pop qword [crash_frame + 16] ; RFLAGS pop qword [crash_frame + 8] ; RSP ; Mark frame as valid mov byte [crash_frame + 152], 1 ; Call Rust handler with error code mov rdi, [crash_frame + 32] ; EC in RDI call isr_page_fault ; Handler never returns cli hlt
Exception handlers that receive an error code must pop it from the stack before accessing the interrupt frame. Exceptions with error codes: 8, 10-14, 17, 21, 29, 30.
The page fault handler displays detailed diagnostic information:
kernel/src/arch/isr_handlers.rs
#[no_mangle]extern "C" fn isr_page_fault(ec: u64) { let cr2: u64; unsafe { core::arch::asm!("mov {r}, cr2", r = out(reg) cr2, options(nostack, preserves_flags)); } let f = frame(); let mut c = Console::new(); let (w, h) = (c.width(), c.height()); // Render diagnostic screen // ... (graphics code omitted) ... let cause = if cr2 < 0x1000 { "Null pointer / acceso a pagina 0" } else if cr2 > 0xFFFF_8000_0000_0000 { "Acceso a espacio de kernel desde usuario" } else if cr2 == f.rip { "Instruccion no ejecutable (NX bit activo)" } else { "Pagina no presente o sin permisos de acceso" }; c.write_at("CAUSA PROBABLE", lp, 158, pal::MID); c.write_at(cause, lp, 172, pal::PF_BLUE); c.present(); halt_loop()}
#[no_mangle]extern "C" fn isr_gp_handler(ec: u64) { let f = frame(); let mut c = Console::new(); let is_ext = (ec & 1) != 0; // External event let is_idt = (ec & 2) != 0; // IDT reference let is_ti = (ec & 4) != 0; // TI bit (LDT vs GDT) let index = (ec >> 3) & 0x1FFF; // Selector index let cause = if ec == 0 { "Instruccion privilegiada / acceso a I/O en modo usuario" } else if is_idt { "Excepcion dentro de handler (IDT corrompida)" } else if index == 0 { "Selector NULL como destino de far call/jump" } else { "Descriptor invalido o sin permisos suficientes" }; // ... render diagnostic screen ... halt_loop()}
The double fault handler runs on a dedicated 16 KB stack (IST1):
kernel/src/arch/isr_handlers.rs
#[no_mangle]extern "C" fn isr_double_fault() { // Write to VGA text mode as last resort unsafe { let v = 0xB8000usize as *mut u16; for i in 0..160usize { core::ptr::write_volatile(v.add(i), 0x4F20); } let msg = b" PORTIX-OS #DF DOUBLE FAULT | SISTEMA DETENIDO "; for (i, &b) in msg.iter().enumerate() { core::ptr::write_volatile(v.add(i), 0x4F00 | b as u16); } } // Then render full diagnostic screen let mut c = Console::new(); // ... (graphics code) ... halt_loop()}
Double fault is a non-recoverable exception. It occurs when an exception is raised while handling another exception (e.g., page fault during page fault handler). The dedicated IST1 stack ensures the handler can execute even if the kernel stack is corrupted.
IRQ0 is handled by a dedicated stub that calls pit_tick():
kernel/src/arch/isr.asm
extern pit_tickglobal irq0_handlerirq0_handler: push rax push rcx push rdx push rsi push rdi push r8 push r9 push r10 push r11 call pit_tick ; Increment tick counter mov al, 0x20 ; Send EOI to PIC out 0x20, al pop r11 pop r10 pop r9 pop r8 pop rdi pop rsi pop rdx pop rcx pop rax iretq
Other IRQs (keyboard, mouse, etc.) are handled by polling in the main loop rather than via interrupts:
kernel/src/main.rs
loop { // Poll PS/2 controller let mut kbd_buf = [0u8; 32]; let mut kbd_n = 0usize; let mut ms_buf = [0u8; 32]; let mut ms_n = 0usize; unsafe { loop { let st = ps2_inb(PS2_STATUS); if st & 0x01 == 0 { break; } // No data available let byte = ps2_inb(PS2_DATA); if st & 0x20 != 0 { // Mouse data if ms_n < 32 { ms_buf[ms_n] = byte; ms_n += 1; } } else { // Keyboard data if kbd_n < 32 { kbd_buf[kbd_n] = byte; kbd_n += 1; } } } } // Process keyboard input for i in 0..kbd_n { if let Some(key) = kbd.feed_byte(kbd_buf[i]) { // Handle key... } } // ...}
Portix uses polling instead of interrupt-driven I/O for keyboard and mouse to simplify the design and avoid re-entrancy issues. IRQs 1 (keyboard) and 12 (mouse) remain masked.