Skip to content

Interrupts and Exceptions

Frank edited this page Jan 18, 2026 · 1 revision

Interrupts and Exceptions

cupid-os implements a comprehensive interrupt and exception handling system that allows the kernel to respond to hardware events and CPU exceptions. This system is crucial for device interaction, error handling, and system stability.

Overview

The interrupt system consists of three main components:

  1. Interrupt Descriptor Table (IDT) - Maps interrupts to handlers
  2. Programmable Interrupt Controller (PIC) - Manages hardware interrupts
  3. Interrupt Service Routines (ISRs) - Handle specific interrupts

CPU Exceptions

CPU exceptions are synchronous events triggered by program execution errors or special instructions. cupid-os handles 32 CPU exceptions (0-31), with detailed information provided for the most common ones.

Exception Types

Vector Exception Description Error Code
0 Division by Zero Division by zero attempted No
1 Debug Debug exception or breakpoint No
2 NMI Non-maskable interrupt No
3 Breakpoint Breakpoint instruction (INT 3) No
4 Overflow INTO instruction overflow No
5 Bound Range BOUND instruction out of range No
6 Invalid Opcode Invalid instruction executed No
7 Device Not Available FPU/MMX/SSE instruction on unavailable device No
8 Double Fault Exception during exception handling Yes
9 Coprocessor Segment Floating-point segment overrun (obsolete) No
10 Bad TSS Invalid Task State Segment Yes
11 Segment Not Present Segment not present in memory Yes
12 Stack-Segment Fault Stack segment fault Yes
13 General Protection Fault General protection violation Yes
14 Page Fault Page not present or access violation Yes
15 Reserved Reserved for future use No
16-31 Various Floating-point and other exceptions Varies

Exception Handling

When an exception occurs, the CPU:

  1. Saves state: Pushes EFLAGS, CS, EIP (and error code if applicable)
  2. Looks up handler: Uses exception vector to find IDT entry
  3. Transfers control: Jumps to the ISR specified in the IDT
  4. Executes handler: ISR processes the exception
  5. Returns: Uses IRET to restore state and continue/resume

Page Fault Exception (Vector 14)

Page faults receive special handling in cupid-os:

if (r->int_no == 14) {
    uint32_t cr2;
    __asm__ volatile("mov %%cr2, %0" : "=r"(cr2));

    // Enable interrupts for interactive prompt
    __asm__ volatile("sti");
    pf_action_t action = shell_pagefault_prompt(r, cr2);
    __asm__ volatile("cli");

    if (action == PF_ACTION_REBOOT) {
        system_reboot();
    }
    // PF_ACTION_CONTINUE: return and retry instruction
    return;
}

Instead of halting, page faults drop into an interactive shell where users can:

  • Inspect the faulting address (CR2)
  • View error code details
  • Choose to continue, reboot, or get more information

Hardware Interrupts (IRQs)

Hardware interrupts are asynchronous events from devices. The PIC manages 16 IRQs (0-15), which are remapped to interrupt vectors 32-47 to avoid conflicts with CPU exceptions.

IRQ Mapping

IRQ Vector Device Typical Use
0 32 Timer System timer (PIT)
1 33 Keyboard PS/2 keyboard
2 34 Cascade Slave PIC
3 35 COM2 Serial port 2
4 36 COM1 Serial port 1
5 37 LPT2 Parallel port 2
6 38 Floppy Floppy disk
7 39 LPT1 Parallel port 1
8 40 RTC Real-time clock
9 41 Free Available
10 42 Free Available
11 43 Free Available
12 44 PS/2 Mouse PS/2 mouse
13 45 FPU Floating-point unit
14 46 IDE Primary IDE
15 47 IDE Secondary IDE

PIC Initialization

The PIC is initialized during kernel startup:

void pic_init(void) {
    // Save current masks
    uint8_t mask1 = inb(PIC1_DATA);
    uint8_t mask2 = inb(PIC2_DATA);

    // ICW1: Start initialization
    outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4);
    outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);

    // ICW2: Set vector offsets (32 for master, 40 for slave)
    outb(PIC1_DATA, 32);
    outb(PIC2_DATA, 40);

    // ICW3: Tell master about slave at IRQ2
    outb(PIC1_DATA, 4);  // Slave at IRQ2
    outb(PIC2_DATA, 2);  // Slave identity

    // ICW4: Set 8086 mode
    outb(PIC1_DATA, ICW4_8086);
    outb(PIC2_DATA, ICW4_8086);

    // Restore masks
    outb(PIC1_DATA, mask1);
    outb(PIC2_DATA, mask2);

    // Enable timer (IRQ0) and keyboard (IRQ1)
    pic_clear_mask(0);
    pic_clear_mask(1);
}

Interrupt Masking

IRQs can be individually masked/unmasked:

void pic_set_mask(uint8_t irq);    // Disable IRQ
void pic_clear_mask(uint8_t irq);  // Enable IRQ

End of Interrupt (EOI)

After handling an interrupt, send EOI to the PIC:

void pic_send_eoi(uint8_t irq) {
    if (irq >= 8)
        outb(PIC2_COMMAND, 0x20);  // EOI to slave PIC
    outb(PIC1_COMMAND, 0x20);      // EOI to master PIC
}

Interrupt Descriptor Table (IDT)

The IDT is a 256-entry table that maps interrupt vectors to handler functions. Each entry contains:

  • Handler address: 32-bit pointer to ISR
  • Segment selector: Code segment (0x08)
  • Gate type: Interrupt gate (0x8E) or trap gate (0x8F)
  • Privilege level: Ring 0 (kernel)
  • Present flag: Entry is valid

IDT Structure

struct idt_entry {
    uint16_t base_low;   // Lower 16 bits of handler address
    uint16_t segment;    // Code segment selector
    uint8_t reserved;    // Always 0
    uint8_t flags;       // Gate type and flags
    uint16_t base_high;  // Upper 16 bits of handler address
};

struct idt_ptr {
    uint16_t limit;      // Size of IDT - 1
    uint32_t base;       // Address of IDT
};

Setting IDT Gates

void idt_set_gate(uint8_t num, uint32_t base, uint16_t sel, uint8_t flags) {
    idt[num].base_low = base & 0xFFFF;
    idt[num].base_high = (base >> 16) & 0xFFFF;
    idt[num].segment = sel;
    idt[num].reserved = 0;
    idt[num].flags = flags;
}

IDT Initialization

During kernel initialization:

void idt_init(void) {
    // Set up IDT pointer
    idtp.limit = (sizeof(struct idt_entry) * 256) - 1;
    idtp.base = (uint32_t)&idt;

    // Clear all entries
    for(int i = 0; i < 256; i++) {
        idt_set_gate(i, 0, 0, 0);
    }

    // Set up exception handlers (vectors 0-14)
    idt_set_gate(0, (uint32_t)isr0, 0x08, 0x8E);
    // ... more exception handlers

    // Set up IRQ handlers (vectors 32-47)
    idt_set_gate(32, (uint32_t)irq0, 0x08, 0x8E);
    // ... more IRQ handlers

    // Load IDT
    load_idt(&idtp);
}

Interrupt Service Routines (ISRs)

ISRs are low-level assembly routines that handle the context switch from user/kernel code to interrupt handler code.

ISR Structure

All ISRs follow the same pattern:

isr0:           ; Example: Division by Zero
    cli         ; Disable interrupts
    push byte 0 ; Push dummy error code (this exception doesn't have one)
    push byte 0 ; Push interrupt number
    jmp isr_common_stub

irq1:           ; Example: Keyboard IRQ
    cli
    push byte 0
    push byte 33 ; Interrupt number (32 + IRQ number)
    jmp irq_common_stub

Common ISR Stub

The common stub handles state preservation:

isr_common_stub:
    pusha           ; Save all general-purpose registers
    push ds         ; Save segment registers
    push es
    push fs
    push gs

    ; Load kernel data segment
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    push esp        ; Push pointer to register structure

    call isr_handler ; Call C handler

    add esp, 4      ; Clean up parameter

    pop gs          ; Restore segment registers
    pop fs
    pop es
    pop ds
    popa           ; Restore general-purpose registers
    add esp, 8     ; Clean up error code and interrupt number
    iret           ; Return from interrupt

Register Structure

The registers structure passed to C handlers:

struct registers {
    uint32_t ds, es, fs, gs;    // Segment registers
    uint32_t edi, esi, ebp, esp, ebx, edx, ecx, eax;  // General registers
    uint32_t int_no, err_code;  // Interrupt number and error code
    uint32_t eip, cs, eflags, useresp, ss;  // Execution state
};

IRQ Handler System

cupid-os implements a flexible IRQ handler system that allows dynamic registration of device drivers.

Handler Registration

typedef void (*irq_handler_t)(struct registers* regs);

void irq_install_handler(int irq, irq_handler_t handler);
void irq_uninstall_handler(int irq);

IRQ Dispatcher

void irq_handler(struct registers* r) {
    int irq = r->int_no - 32;  // Convert vector to IRQ number

    if (irq_handlers[irq]) {
        irq_handlers[irq](r);  // Call registered handler
    }

    pic_send_eoi(irq);  // Send EOI to PIC
}

Example: Keyboard Handler

void keyboard_isr(struct registers* regs) {
    uint8_t scancode = inb(KEYBOARD_DATA_PORT);
    keyboard_enqueue_scancode(scancode);
    pic_send_eoi(IRQ_KEYBOARD);
}

Initialization Sequence

The interrupt system is initialized in this order:

  1. PIC Initialization: Remap IRQs to avoid conflicts
  2. IDT Setup: Create IDT entries for exceptions and IRQs
  3. ISR Installation: Load IDT into processor
  4. IRQ Handler Setup: Install default handlers
  5. Device Initialization: Register device-specific handlers
void kernel_main() {
    // ... other initialization

    pic_init();     // Set up PIC
    idt_init();     // Set up IDT and load it
    irq_init();     // Set up IRQ subsystem

    // Install device handlers
    keyboard_init();
    timer_init();

    // Enable interrupts
    __asm__ volatile("sti");

    // ... start shell
}

Debugging Interrupt Issues

Common Problems

Triple Faults

  • Cause: Invalid IDT entry or corrupted stack
  • Debug: Check GDT setup, IDT loading, stack pointer

Spurious Interrupts

  • Cause: PIC not properly initialized
  • Debug: Verify ICW sequences, check masks

ISR Not Called

  • Cause: Wrong vector number or flags
  • Debug: Check IDT entries, verify interrupt numbers

System Freezes

  • Cause: ISR doesn't return or sends wrong EOI
  • Debug: Add debug prints, check EOI timing

Debugging Techniques

Visual Debugging

void debug_isr(struct registers* r) {
    print("ISR called: ");
    print_int(r->int_no);
    print("\n");
}

Register Inspection

void dump_registers(struct registers* r) {
    print("EIP: "); print_hex(r->eip);
    print(" CS: "); print_hex(r->cs);
    print(" EFL: "); print_hex(r->eflags);
    print("\n");
}

PIC State Checking

uint8_t pic_get_mask(int pic) {
    uint16_t port = (pic == 0) ? PIC1_DATA : PIC2_DATA;
    return inb(port);
}

Performance Considerations

ISR Optimization

  • Keep ISRs short: Do minimal work, defer processing
  • Avoid function calls: Inline critical operations
  • Minimize stack usage: Be careful with local variables

Interrupt Latency

  • Disable interrupts sparingly: Use cli/sti carefully
  • Prioritize IRQs: Handle critical interrupts first
  • Use interrupt levels: If hardware supports it

Memory Barriers

  • Compiler barriers: Use volatile for shared variables
  • CPU barriers: Consider memory fence instructions if needed

Extending the System

Adding New Interrupts

  1. Choose vector: Find unused interrupt vector (48-255)
  2. Create ISR: Add assembly stub and C handler
  3. Set IDT gate: Register handler in IDT
  4. Handle interrupt: Implement device-specific logic

Adding Device Drivers

  1. Implement handler: Create IRQ handler function
  2. Register handler: Use irq_install_handler()
  3. Initialize device: Set up hardware, enable interrupts
  4. Handle data: Process interrupts in handler

Advanced Features

  • Interrupt prioritization: Implement priority schemes
  • Nested interrupts: Allow interrupts during handlers
  • Interrupt threads: Move handler processing to threads

References

  • Intel Architecture Manuals: Detailed interrupt specifications
  • OSDev Wiki: PIC programming and IDT setup guides
  • 8259A Datasheet: PIC hardware specifications

For more information about system components:

Clone this wiki locally