## 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: ```22:37:/home/lochlan/Documents/Coding/c/os/idt.c 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: ```42:47:/home/lochlan/Documents/Coding/c/os/idt.c 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: ```53:67:/home/lochlan/Documents/Coding/c/os/idt.c 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: ```69:73:/home/lochlan/Documents/Coding/c/os/idt.c 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: ```80:116:/home/lochlan/Documents/Coding/c/os/idt.c 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: ```124:136:/home/lochlan/Documents/Coding/c/os/idt.c 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: ```138:144:/home/lochlan/Documents/Coding/c/os/idt.c 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: ```150:177:/home/lochlan/Documents/Coding/c/os/idt.c 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 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: ```183:214:/home/lochlan/Documents/Coding/c/os/idt.c 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 0–31, 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**: - `id_tinit` 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.