8.6 KiB
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 0–31 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, andRFLAGSat the time of the fault.
The handler's behaviour is:
- For hardware IRQs (vectors 32–47):
- Send an End Of Interrupt (EOI) to the PIC.
- Return to the interrupted context.
- For CPU exceptions (vectors 0–31):
- 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:
- Capture firmware IDT:
sidtreads the current IDTR intoold_idtr.old_idtis set to the base of the firmware's IDT.
- Copy firmware entries:
- For all indices
iwhere the address lies within the firmware's IDT limit, copy the existing entry into the kernel'sidtarray. - For indices beyond the firmware's limit, install the kernel's own stub from
isr_stub_table.
- For all indices
- Override CPU exceptions:
- For vectors 0–31, call
idt_set_gatewith the kernel's stubs, ensuring that exceptions are always handled byisr_handler.
- For vectors 0–31, call
- Activate new IDT:
- Populate
idtrwith the address and size of the kernel'sidt. - Call
lidtto load the new IDT.
- Populate
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_initstoresBootingBootso thatisr_handlercan safely useBoot->printfor diagnostics.
- Memory subsystem:
isr_handlerreads CR2 for page faults; combined withpaging_get_physfrommemory.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.
- The current implementation is non-preemptive: context switches happen only through explicit calls to
- 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_yieldto add preemptive scheduling. - Extending
ISRFrameandisr_handlerwith richer diagnostics or a kernel debugger stub.