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

270 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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:
```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 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:
```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 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.