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

Signal Callback

A signal callback system does a couple really important things we are going to need.

First off, it means that any part of the kernel can register a function to be called when a specific interrupt fires. It also means that instead of hardcoding handlers in the IDT, I can have a table of function pointers that can be swapped out at runtime. Part of this is also the ability to handle signals like Ctrl+C, Ctrl+Z and so on.

So instead of:

#![allow(unused)]
fn main() {
// Hardcoded forever
IDT.entries[33].set_handler(keyboard_interrupt_handler);
}

I can have:

#![allow(unused)]
fn main() {
// Registerable at runtime
signal::register(SIGKEYBOARD, my_custom_handler);
}

This sets the foundation for processes later — each process can register its own signal handlers, just like in Linux with signal(SIGINT, my_handler).

The Signal Table

Since there are exactly 256 possible interrupts on x86, we don’t need a dynamic data structure. A fixed array of 256 function pointers does the job cleanly with no heap allocation:

#![allow(unused)]
fn main() {
type SignalHandler = fn(interrupt: u32, frame: &InterruptFrame);
static mut SIGNAL_TABLE: [Option<SignalHandler>; 256] = [None; 256];
}

Option<SignalHandler> means each slot is either None (no handler registered) or Some(fn) (handler registered). The registration API is simple:

#![allow(unused)]
fn main() {
pub fn register(interrupt: u8, handler: SignalHandler) {
    unsafe {
        SIGNAL_TABLE[interrupt as usize] = Some(handler);
    }
}
}

During init, we pre-populate the table with default handlers so nothing is ever unhandled:

#![allow(unused)]
fn main() {
pub fn init() {
    register(0, handlers::divide_by_zero);
    register(6, handlers::invalid_opcode);
    register(8, handlers::double_fault);
    register(13, handlers::general_protection_fault);
    register(14, handlers::page_fault);
    register(33, keyboard::kbhandler);
    for i in 0..=255u8 {
        if SIGNAL_TABLE[i as usize].is_none() {
            register(i, handlers::default_interrupt);
        }
    }
}
}

Generic Dispatcher

Instead of each IDT entry pointing to its own specific handler, they all point to assembly stubs that push their interrupt number and call a single rust_interrupt_dispatcher. The flow changes from:

IRQ fires → IDT → hardcoded handler

To:

IRQ fires → IDT → generic dispatcher → looks up SIGNAL_TABLE[n] → calls registered handler

The dispatcher in Rust:

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn rust_interrupt_dispatcher(interrupt_num: u32, frame: &InterruptFrame) {
    unsafe {
        if let Some(handler) = SIGNAL_TABLE[interrupt_num as usize] {
            handler(interrupt_num, frame);
        }
    }
}
}

This is a significant improvement — adding a new interrupt handler is now just one register() call rather than touching the IDT directly.