Files
Operator-system/docs/interrupts-and-exceptions.md
2026-02-27 21:04:56 +00:00

8.6 KiB
Raw Blame History

Interrupt and exception handling overview

The kernel's interrupt/exception handling is implemented in idt.c and a companion assembly file isr.S (not shown here). The design goals are:

  • Preserve the firmware's existing hardware interrupt handlers where possible.
  • Override CPU exception vectors 031 with kernel stubs that route into a C dispatcher.
  • Print detailed diagnostics for exceptions and halt on unrecoverable faults.

IDT representation

The x86-64 Interrupt Descriptor Table (IDT) is represented as an array of packed 16-byte entries:

typedef struct {
    UINT16 offset_low;      /* bits  0-15 of handler address */
    UINT16 selector;        /* code segment selector         */
    UINT8  ist;             /* interrupt stack table index    */
    UINT8  type_attr;       /* type and attributes           */
    UINT16 offset_mid;      /* bits 16-31 of handler address */
    UINT32 offset_high;     /* bits 32-63 of handler address */
    UINT32 zero;            /* reserved, must be zero        */
} __attribute__((packed)) IdtEntry;

typedef struct {
    UINT16 limit;
    UINT64 base;
} __attribute__((packed)) IdtPtr;

Module state:

static IdtEntry  idt[IDT_SIZE];
static BootInfo *gBoot = NULL;

/* Defined in isr.S  one stub function per vector (0-255). */
extern void (*isr_stub_table[])(void);

Each element of isr_stub_table is a low-level assembly stub that:

  • Saves CPU state into an ISRFrame.
  • Calls the common C dispatcher isr_handler.
  • Restores state and performs iretq (for IRQs) or loops into a halt (for fatal faults).

Installing IDT entries

idt_set_gate encodes a handler function pointer into an IDT entry:

static void idt_set_gate(UINTN index, void (*handler)(void))
{
    UINT64 addr = (UINT64)(UINTN)handler;
    UINT16 selector = 0;

    __asm__ __volatile__("mov %%cs, %0" : "=r"(selector));

    idt[index].offset_low = (UINT16)(addr & 0xFFFF);
    idt[index].selector = selector;
    idt[index].ist = 0;
    idt[index].type_attr = IDT_TYPE_INTERRUPT;
    idt[index].offset_mid = (UINT16)((addr >> 16) & 0xFFFF);
    idt[index].offset_high = (UINT32)((addr >> 32) & 0xFFFFFFFF);
    idt[index].zero = 0;
}

lidt loads a new IDTR:

static void lidt(const IdtPtr *idtr)
{
    __asm__ __volatile__("lidt (%0)" :: "r"(idtr));
}

Exception names

For better diagnostics, the kernel maps exception vectors to human-readable names:

static const CHAR16 *exception_name(UINTN vector)
{
    switch (vector) {
    case 0: return L"Divide Error";
    case 1: return L"Debug";
    ...
    case 14: return L"Page Fault";
    ...
    case 30: return L"Security";
    case 31: return L"Reserved";
    default: return L"Unknown";
    }
}

These strings are used by isr_handler when printing exception banners.


PIC helpers and halting

The kernel provides minimal support for acknowledging legacy PIC interrupts and halting the CPU in fatal cases:

static inline void outb(UINT16 port, UINT8 value)
{
    __asm__ __volatile__("outb %0, %1" :: "a"(value), "Nd"(port));
}

static void pic_eoi(UINTN vector)
{
    if (vector >= 40) {
        outb(0xA0, 0x20);  /* EOI to slave PIC */
    }
    outb(0x20, 0x20);      /* EOI to master PIC */
}

halt_forever disables interrupts and executes hlt in an infinite loop:

static void halt_forever(void)
{
    for (;;) {
        __asm__ __volatile__("cli; hlt");
    }
}

ISR dispatcher (isr_handler)

isr_handler is the central C function that receives control from all exception and interrupt stubs:

void isr_handler(ISRFrame *frame)
{
    UINT64 cr2 = 0;

    /* Hardware IRQs (vectors 32-47): send EOI and return */
    if (frame->vector >= 32 && frame->vector <= 47) {
        pic_eoi(frame->vector);
        return;
    }

    /* CPU exceptions (vectors 0-31): print diagnostics and halt */
    if (gBoot != NULL && gBoot->print != NULL) {
        gBoot->print(L"\n\rEXCEPTION: %d (%s)\n\r", frame->vector,
                     exception_name(frame->vector));
        gBoot->print(L"  Error Code: 0x%lx\n\r", frame->error_code);
        gBoot->print(L"  RIP: 0x%lx  CS: 0x%lx  RFLAGS: 0x%lx\n\r",
                     frame->rip, frame->cs, frame->rflags);
    }

    if (frame->vector == 14) {
        __asm__ __volatile__("mov %%cr2, %0" : "=r"(cr2));
        if (gBoot != NULL && gBoot->print != NULL) {
            gBoot->print(L"  CR2: 0x%lx\n\r", cr2);
        }
    }

    halt_forever();
}

The ISRFrame structure (defined in idt.h) contains the state saved by the assembly stubs, including:

  • Exception/interrupt vector number.
  • Error code (if applicable).
  • RIP, CS, and RFLAGS at the time of the fault.

The handler's behaviour is:

  • For hardware IRQs (vectors 3247):
    • Send an End Of Interrupt (EOI) to the PIC.
    • Return to the interrupted context.
  • For CPU exceptions (vectors 031):
    • Print a diagnostic header with the vector number and name.
    • Show error code and execution context (RIP, CS, RFLAGS).
    • For page faults (vector 14), read and print CR2 (faulting virtual address).
    • Halt the system via halt_forever.

This makes debugging faults significantly easier when running under QEMU or on real hardware.


IDT initialisation (idt_init)

idt_init is called from kmain early during boot:

void idt_init(BootInfo *Boot)
{
    IdtPtr old_idtr;
    IdtPtr idtr;
    IdtEntry *old_idt = NULL;
    UINTN i = 0;

    gBoot = Boot;

    /* Read the firmware's existing IDT so we can preserve its entries */
    __asm__ __volatile__("sidt %0" : "=m"(old_idtr));
    old_idt = (IdtEntry *)(UINTN)old_idtr.base;

    /* Copy the entire existing IDT first (preserves firmware IRQ handlers) */
    for (i = 0; i < IDT_SIZE; i++) {
        if (old_idt != NULL && (i * sizeof(IdtEntry)) < (UINTN)(old_idtr.limit + 1)) {
            idt[i] = old_idt[i];
        } else {
            idt_set_gate(i, isr_stub_table[i]);
        }
    }

    /* Override only CPU exception vectors (0-31) with our handlers */
    for (i = 0; i < 32; i++) {
        idt_set_gate(i, isr_stub_table[i]);
    }

    idtr.limit = (UINT16)(sizeof(idt) - 1);
    idtr.base = (UINT64)(UINTN)idt;

    lidt(&idtr);
}

The process is:

  1. Capture firmware IDT:
    • sidt reads the current IDTR into old_idtr.
    • old_idt is set to the base of the firmware's IDT.
  2. Copy firmware entries:
    • For all indices i where the address lies within the firmware's IDT limit, copy the existing entry into the kernel's idt array.
    • For indices beyond the firmware's limit, install the kernel's own stub from isr_stub_table.
  3. Override CPU exceptions:
    • For vectors 031, call idt_set_gate with the kernel's stubs, ensuring that exceptions are always handled by isr_handler.
  4. Activate new IDT:
    • Populate idtr with the address and size of the kernel's idt.
    • Call lidt to load the new IDT.

This approach preserves any firmware-installed handlers for higher interrupt vectors (e.g., hardware IRQs or system-specific events), while guaranteeing full control over CPU exception handling.


Interaction with the rest of the kernel

The IDT/ISR subsystem interacts with other parts of the kernel in the following ways:

  • BootInfo access:
    • idt_init stores Boot in gBoot so that isr_handler can safely use Boot->print for diagnostics.
  • Memory subsystem:
    • isr_handler reads CR2 for page faults; combined with paging_get_phys from memory.c, this can be used to inspect paging state.
  • Tasks and scheduler:
    • The current implementation is non-preemptive: context switches happen only through explicit calls to task_yield, not timer interrupts. Exceptions still interrupt tasks asynchronously, but there is no timer tick driving the scheduler.
  • User-level diagnostics:
    • When an exception occurs, the on-screen diagnostics provide enough context to identify the type of fault and its location in the kernel, especially when used alongside a symbol-enabled build and external debugger.

Future extensions might include:

  • Installing a periodic timer IRQ handler that calls task_yield to add preemptive scheduling.
  • Extending ISRFrame and isr_handler with richer diagnostics or a kernel debugger stub.