IDT and Interrupts: Making the CPU Actually Do Something Useful
Trying to figure out what IDT is, how to implement it, and how to use it to make the CPU actually do something useful.
It's been about two weeks since I started this OS project, and I've finally gotten the Interrupt Descriptor Table (IDT) set up properly. For anyone not familiar with OS development, the IDT is essentially a table that tells the CPU what code to run when various interrupts occur. Without interrupts, you basically have a useless system that can't respond to anything - no keyboard input, no timer, nothing.
I spent most of last weekend figuring out why my interrupts weren't firing, only to discover I had forgotten to enable interrupts after initializing the IDT. Classic rookie mistake:
asm volatile("sti"); // This one line cost me six hours of debuggingSetting up the actual IDT structure wasn't too bad, just tedious. Each entry is a gate descriptor that points to the handler function for that interrupt:
struct idt_entry {
uint16_t base_lo; // Lower 16 bits of handler function address
uint16_t sel; // Kernel segment selector
uint8_t always0; // Must be zero
uint8_t flags; // Flags like privilege level and type
uint16_t base_hi; // Upper 16 bits of handler function address
} __attribute__((packed));
struct idt_ptr {
uint16_t limit;
uint32_t base;
} __attribute__((packed));
static struct idt_entry idt_entries[256];
static struct idt_ptr idt_ptr;The real challenge came when I started remapping the Programmable Interrupt Controller (PIC). The default configuration has IRQs mapped to interrupts 0-15, which conflicts with CPU exceptions. So you have to remap them to different interrupt numbers (typically 32-47). This involves sending a series of initialization commands to the two PIC chips:
// Remap the PICs to use interrupts 32-47
void pic_remap(void) {
outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4);
outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);
outb(PIC1_DATA, 32); // Remap IRQ0-7 to interrupts 32-39
outb(PIC2_DATA, 40); // Remap IRQ8-15 to interrupts 40-47
outb(PIC1_DATA, 4); // Tell PIC1 that PIC2 is at IRQ2
outb(PIC2_DATA, 2); // Tell PIC2 its cascade identity
outb(PIC1_DATA, ICW4_8086);
outb(PIC2_DATA, ICW4_8086);
// Unmask all interrupts
outb(PIC1_DATA, 0);
outb(PIC2_DATA, 0);
}At this point, I should mention I'm working with OSDev Wiki alongside the Little Book. The wiki has been invaluable, especially for these hardware-specific details that the book glosses over.
After getting the basic interrupt infrastructure set up, I implemented exception handlers for common CPU exceptions like divide-by-zero, general protection fault, and page fault. Nothing quite like intentionally causing a divide-by-zero exception to see if your handler works:
// Let's test our exception handling
int test_zero_div() {
int a = 10;
int b = 0;
return a / b; // Should trigger INT 0 (divide by zero)
}When I ran this, I got my first proper kernel panic! Which, in OS development, counts as progress. The correct exception was triggered and my handler displayed the error. I've never been so happy to see a crash.
I needed a keyboard handler next, and I decided to go with the simplest possible solution for now that just reads scancodes from 0x60 and converts them to ASCII:
static void keyboard_handler(registers_t regs) {
uint8_t scancode = inb(0x60);
// Handle the scancode
if (scancode & 0x80) {
// Key release
} else {
// Key press
char c = scancode_to_ascii(scancode);
// Do something with the character
}
// Send End of Interrupt to PIC
outb(PIC1_COMMAND, PIC_EOI);
}This almost certainly needs a refactor in the future but it's gonna work for now.
I've also implemented a basic timer interrupt using the Programmable Interval Timer (PIT), which fires at a configurable frequency (I went with 100Hz to start):
static void timer_handler(registers_t regs) {
static uint32_t tick = 0;
tick++;
// Do something with tick
if (tick % 100 == 0) {
// Every second
}
outb(PIC1_COMMAND, PIC_EOI);
}Right now I'm stuck on setting up paging to enable virtual memory, which is how we do process isolation. I've been reading ahead in the Little Book and it seems like paging is where the real complexity begins.
I'm actually really proud of myself for making it this far. It's worth noting that I haven't hit the really challenging parts yet but each milestone feels like a real accomplishment, and I'm learning a lot about the x86 architecture that I never knew before despite working with computers for years. I will say though, the itch to rewrite in ARM assembly is growing.