Files
LithosAnanake/src/starkernel/arch/amd64/interrupts.c
T
Robert Allan JamesandClaude Sonnet 5 88eb73cfe8 starkernel: item 4.3.5 -- amd64 I/O APIC + i8042 keyboard, interrupt-driven
Punch list §25 item 4.3.5 complete.

New ioapic.c/i8042.c drivers (MADT-derived I/O APIC base, no hardcoded
constants) plus a KBD-SCAN/KBD-DEBUG diagnostic word pair. Three real
bugs found and fixed en route, all blocking this item's own acceptance:
a fatal LAPIC spurious-vector crash (nothing had driven a real external
interrupt through the I/O APIC before), OVMF leaving the keyboard device
itself scanning-disabled (0xF4 fix), and isr.S's stub table only having
individually-numbered stubs through vector 32 -- everything above that,
including our IRQ1 vector 33, silently reported as vector 255 regardless
of which IDT slot actually fired. Verified live via QEMU sendkey against
KBD-SCAN: correct XT Set-1 make/break codes for two different keys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 00:18:48 -04:00

477 lines
18 KiB
C
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.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* interrupts.c (amd64) - Interrupt setup and exception/IRQ handling
*/
#include <stdint.h>
#include "arch.h"
#include "console.h"
#include "apic.h"
#include "timer.h"
#include "starkernel/i8042.h"
/* Set by the FORTH dispatcher just before calling entry->func(vm).
* Printed on fault to identify which word was executing. */
volatile const char *g_sk_fault_word = (void *)0;
/* Count of LAPIC spurious-vector interrupts since boot (item 4.3.5). */
volatile uint32_t g_spurious_count = 0;
#define IDT_ENTRIES 256
#define INTERRUPT_GATE 0x8E
/**
* @brief Write a single byte to an x86 I/O port via @c OUT.
*
* Issues the @c OUT instruction to port @p port with data byte @p val.
* Used exclusively to program the legacy 8259 PIC's interrupt-mask
* registers during @c pic_disable(). In a kernel that uses the Local APIC
* for all interrupt delivery the PIC must be fully masked before enabling
* CPU interrupts; otherwise spurious IRQ0 from the PIC timer can corrupt
* the APIC timer ISR path.
*
* The @c "Nd" constraint allows the assembler to emit the short-form
* @c OUT @p imm8 encoding for constants in [0, 255] and the register form
* @c OUT @c DX otherwise.
*
* @param port 16-bit I/O port number (e.g., 0x21 = PIC1 data, 0xA1 = PIC2 data).
* @param val Byte value to write to the port.
*/
static inline void outb(uint16_t port, uint8_t val)
{
__asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
}
struct idt_entry {
uint16_t offset_low;
uint16_t selector;
uint8_t ist;
uint8_t type_attr;
uint16_t offset_mid;
uint32_t offset_high;
uint32_t zero;
} __attribute__((packed));
struct idtr {
uint16_t limit;
uint64_t base;
} __attribute__((packed));
extern void *isr_stub_table[IDT_ENTRIES] __attribute__((visibility("hidden")));
/*
* Assembly hands us:
* - vector
* - error_code (normalized to 0 if not present)
* - rip, cs, rflags
* - cr2 (captured for convenience; meaningful primarily for #PF)
*/
extern void isr_common_handler(uint64_t vector,
uint64_t error_code,
uint64_t rip,
uint64_t cs,
uint64_t rflags,
uint64_t cr2,
uint64_t stack_frame_ptr);
static struct idt_entry idt[IDT_ENTRIES];
/**
* @brief Write a single 64-bit interrupt gate descriptor into the IDT.
*
* Fills @c idt[@p vector] with an x86-64 interrupt gate (type 0x8E):
* - @c offset_low / @c offset_mid / @c offset_high — three parts of the
* ISR handler address @p isr split across the 16-byte descriptor layout.
* - @c selector = 0x08 — kernel code segment (GDT slot 1).
* - @c ist = 0 — no interrupt stack table; uses the current RSP.
* - @c type_attr = @c INTERRUPT_GATE (0x8E) — P=1, DPL=0, 64-bit interrupt gate;
* the CPU automatically clears RFLAGS.IF on entry.
* - @c zero = 0 — upper reserved dword must be zero.
*
* Called once per vector inside @c arch_interrupts_init()'s loop after
* the relocation offset has been applied to the stub address from
* @c isr_stub_table.
*
* @param vector IDT vector index [0, 255].
* @param isr Runtime address of the per-vector assembly stub in @c isr.S.
*/
static void set_idt_entry(int vector, void *isr) {
uint64_t addr = (uint64_t)isr;
idt[vector].offset_low = (uint16_t)(addr & 0xFFFFu);
idt[vector].selector = 0x08; /* kernel code selector */
idt[vector].ist = 0;
idt[vector].type_attr = INTERRUPT_GATE;
idt[vector].offset_mid = (uint16_t)((addr >> 16) & 0xFFFFu);
idt[vector].offset_high = (uint32_t)((addr >> 32) & 0xFFFFFFFFu);
idt[vector].zero = 0;
}
/**
* @brief Load the Interrupt Descriptor Table register (@c LIDT).
*
* Executes the @c LIDT instruction with the descriptor at @p idtr_desc,
* which contains the 16-bit limit (byte size of the IDT minus 1) and the
* 64-bit linear base address of the IDT array. After this instruction the
* CPU uses the new IDT for all exception and interrupt dispatch.
*
* Must be called after all 256 entries in @c idt[] have been filled by
* @c set_idt_entry(). Called at the end of @c arch_interrupts_init().
*
* @param idtr_desc Pointer to an @c idtr struct containing limit and base.
*/
static void lidt(struct idtr *idtr_desc) {
__asm__ volatile ("lidt (%0)" :: "r"(idtr_desc));
}
/**
* @brief Print a 64-bit value as 16 lowercase hex digits to the kernel console.
*
* Emits exactly 16 nibbles (no @c 0x prefix, no separators) by iterating
* from bit 60 down to bit 0 in steps of 4. Used exclusively by
* @c isr_common_handler() to print register values (RIP, CR2, RSP, RBP,
* return address) in the exception diagnostic dump. No allocation or libc
* dependency.
*
* @param value 64-bit value to format as lowercase hex.
*/
static void print_hex64(uint64_t value) {
for (int i = 60; i >= 0; i -= 4) {
uint8_t nib = (uint8_t)((value >> (uint64_t)i) & 0xFu);
console_putc((char)(nib < 10 ? ('0' + nib) : ('a' + (nib - 10))));
}
}
/**
* @brief Print a 64-bit unsigned integer in decimal to the kernel console.
*
* Converts @p v to a decimal ASCII string using a 32-byte stack buffer
* (digit reversal technique) and emits each character via @c console_putc().
* Prints "0" for a zero value. Used by @c isr_common_handler() to print
* the exception vector number in decimal alongside its hex representation.
* No allocation or libc dependency.
*
* @param v 64-bit unsigned integer to print.
*/
static void print_dec_u64(uint64_t v) {
/* tiny decimal printer, no malloc, no libc */
char buf[32];
int i = 0;
if (v == 0) {
console_putc('0');
return;
}
while (v > 0 && i < (int)sizeof(buf)) {
buf[i++] = (char)('0' + (v % 10u));
v /= 10u;
}
while (i-- > 0) {
console_putc(buf[i]);
}
}
/**
* @brief Fully mask the legacy 8259A PIC to suppress all legacy IRQs.
*
* Writes 0xFF to PIC1's interrupt-mask register (port 0x21) and PIC2's
* interrupt-mask register (port 0xA1). When all bits are set every IRQ
* line (IRQ0IRQ15) is masked and the PIC will not assert INTR to the CPU.
*
* This is necessary because UEFI firmware initialises the 8259 PIC in
* compatibility mode with IRQ0 (timer) running at 18.2 Hz. After
* @c ExitBootServices() the firmware no longer handles these IRQs; if the
* PIC is not masked before @c STI, spurious PIC interrupts are delivered
* to the kernel's IDT at legacy vectors 0x200x2F, most of which have not
* been set up as ISRs and will triple-fault.
*
* Called from @c arch_interrupts_init() after the IDT is loaded but
* before @c arch_enable_interrupts().
*/
static void pic_disable(void)
{
/* Mask all IRQs on PIC1 and PIC2 to avoid spurious legacy interrupts */
outb(0x21, 0xFF);
outb(0xA1, 0xFF);
}
/**
* @brief Decode and print the x86-64 Page Fault error code to the kernel console.
*
* The processor pushes a 32-bit error code onto the stack for a #PF
* (vector 14). Each bit encodes information about the faulting access:
*
* - bit 0 (P): 0 = page not present; 1 = protection violation.
* - bit 1 (W/R): 0 = read access; 1 = write access.
* - bit 2 (U/S): 0 = supervisor mode access; 1 = user mode access.
* - bit 3 (RSVD): 1 = fault caused by a reserved bit set in a PTE.
* - bit 4 (I/D): 1 = instruction fetch (no-execute violation).
* - bit 5 (PK): 1 = protection-key violation (if PKRU is active).
* - bit 6 (SS): 1 = shadow-stack access violation (CET).
* - bit 15 (SGX): 1 = SGX enclave access violation.
*
* Prints each active flag as a short tag to the kernel console followed
* by a newline. The CR2 register (captured by the ISR stub) holds the
* faulting linear address and is printed separately by
* @c isr_common_handler().
*
* @param ec 64-bit page fault error code (upper 32 bits are always zero
* on current x86-64 hardware; tested up to bit 15 per SDM).
*/
static void decode_pf_error(uint64_t ec)
{
/*
* Page Fault Error Code bits:
* bit0 P: 0=non-present, 1=protection violation
* bit1 W/R: 0=read, 1=write
* bit2 U/S: 0=supervisor, 1=user
* bit3 Rsvd: 1=reserved bit violation
* bit4 I/D: 1=instruction fetch
* bit5 PK: 1=protection key violation (if supported)
* bit6 SS: 1=shadow stack (if supported)
* bit15 SGX: 1=SGX (if supported)
*/
console_puts("PF EC : ");
console_puts((ec & 0x1u) ? "PROT " : "NP ");
console_puts((ec & 0x2u) ? "W " : "R ");
console_puts((ec & 0x4u) ? "USR " : "SUP ");
if (ec & 0x8u) console_puts("RSVD ");
if (ec & 0x10u) console_puts("IFETCH ");
if (ec & 0x20u) console_puts("PK ");
if (ec & 0x40u) console_puts("SS ");
if (ec & 0x8000u) console_puts("SGX ");
console_println("");
}
/**
* @brief Unified C-level interrupt and exception handler.
*
* All 256 IDT entries funnel through a common assembly trampoline in
* @c isr.S which captures caller-saved registers, reads @c CR2 (the
* page-fault linear address), normalises the error code to 0 for vectors
* that do not push one, and then calls this function with six arguments
* reconstructed from the interrupt stack frame.
*
* **APIC timer path (vector == @c APIC_TIMER_VECTOR):**
* Calls @c heartbeat_tick() to update the rolling window of inter-tick TSC
* deviations, then issues @c apic_eoi() and returns. This is the hot path
* — it executes at 100 Hz during normal kernel operation and must be kept
* brief.
*
* **Exception path (all other vectors):**
* Prints a structured diagnostic to the kernel console:
* - Vector number (decimal and hex)
* - Error code (hex)
* - RIP (hex) — faulting instruction pointer
* - CR2 (hex) — page-fault linear address (meaningful only for vector 14)
* - RSP (derived from @p stack_frame_ptr + 160, covering the ISR stub's
* pushed register block)
* - RBP (read from the saved-register block at @p stack_frame_ptr + 64)
* - Return address (first quadword at the derived RSP)
* - Vector-specific decoding: @c #DE (0), @c #GP (13) with the active FORTH
* word name from @c g_sk_fault_word, @c #PF (14) with @c decode_pf_error().
* Then halts the CPU in an infinite @c HLT loop — exceptions are fatal.
*
* The @p cs and @p rflags parameters are currently unused (suppressed with
* @c (void) casts) but retained in the signature for future use (e.g.,
* printing privilege level on #GP).
*
* @param vector IDT vector that fired [0, 255].
* @param error_code Error code from the CPU stack (0 if not applicable).
* @param rip Faulting or interrupted instruction pointer.
* @param cs Code segment selector at the time of the interrupt.
* @param rflags RFLAGS at the time of the interrupt.
* @param cr2 Value of @c CR2 (page-fault linear address; only
* architecturally meaningful for vector 14).
* @param stack_frame_ptr Address of the ISR stub's saved-register block on
* the stack; used to reconstruct RSP and RBP for the
* diagnostic dump.
*/
void isr_common_handler(uint64_t vector,
uint64_t error_code,
uint64_t rip,
uint64_t cs,
uint64_t rflags,
uint64_t cr2,
uint64_t stack_frame_ptr)
{
/* Handle APIC timer interrupt (heartbeat) */
if (vector == APIC_TIMER_VECTOR) {
apic_timer_rearm();
heartbeat_tick();
/* Acknowledge interrupt and return (don't halt!) */
apic_eoi();
return;
}
/* Handle i8042 keyboard interrupt (item 4.3.5) */
if (vector == I8042_KEYBOARD_VECTOR) {
i8042_handle_irq();
apic_eoi();
return;
}
/* Spurious interrupt: normal occasional race per Intel SDM Vol.3 §10.9,
* not a fault. No EOI -- the SDM is explicit that spurious-vector
* interrupts must not be acknowledged. g_spurious_count is a cheap,
* permanent diagnostic (item 4.3.5 found a real keyboard IRQ silently
* misreported as spurious due to a missing dedicated ISR stub -- see
* isr.S -- so a way to notice "spurious firing when it shouldn't be"
* is worth keeping, not just scaffolding for that one investigation). */
if (vector == APIC_SPURIOUS_VECTOR) {
g_spurious_count++;
return;
}
/* All other vectors are exceptions - print diagnostic and halt */
console_println("\n=== INTERRUPT/EXCEPTION ===");
console_puts("Vector : ");
print_dec_u64(vector);
console_puts(" (0x");
print_hex64(vector);
console_println(")");
console_puts("Error : 0x"); print_hex64(error_code); console_println("");
(void)cs;
(void)rflags;
console_puts("RIP : 0x"); print_hex64(rip); console_println("");
console_puts("CR2 : 0x"); print_hex64(cr2); console_println("");
uint64_t fault_rsp = stack_frame_ptr + 160u;
console_puts("RSP : 0x"); print_hex64(fault_rsp); console_println("");
console_puts("RBP : 0x");
print_hex64(*((uint64_t *)(uintptr_t)(stack_frame_ptr + 64u)));
console_println("");
console_puts("RET : 0x");
print_hex64(*((uint64_t *)(uintptr_t)fault_rsp));
console_println("");
switch (vector) {
case 0:
console_println("Fault: Divide Error (#DE)");
break;
case 13:
console_println("Fault: General Protection (#GP)");
if (g_sk_fault_word) {
console_puts("Word : ");
console_println((const char *)g_sk_fault_word);
}
break;
case 14:
console_println("Fault: Page Fault (#PF)");
decode_pf_error(error_code);
break;
default:
console_println("Fault: Unhandled vector");
break;
}
console_println("Halting.");
while (1) {
arch_halt();
}
}
/*
* Compute the runtime load offset.
* The linker places isr_stub0 at a known link-time address. At runtime,
* we compare that to the actual address to determine the relocation delta.
* This is necessary because UEFI PE loading doesn't apply relocations to
* addresses stored in the .data section (isr_stub_table).
*/
extern void isr_stub0(void) __attribute__((visibility("hidden"))); /* Defined in isr.S */
/**
* @brief Initialise the x86-64 IDT and disable the legacy 8259 PIC (M4).
*
* This is the M4 milestone function called from @c kernel_main() after the
* GDT is live (@c arch_early_init()) and before @c arch_enable_interrupts().
* It performs three operations:
*
* 1. **Relocation fixup for the ISR stub table.**
* @c isr.S exports @c isr_stub_table[], a 256-entry table of link-time
* function pointers assembled at their expected virtual addresses. In a
* UEFI PE32+ image the UEFI loader places the image at an arbitrary base;
* pointer-sized relocations in @c .data are not applied by the PE loader
* (only code-relative relocations in @c .text are). The fixup computes the
* runtime delta between @c isr_stub0's runtime address and its link-time
* address stored in @c isr_stub_table[0], then applies that delta to every
* entry before installing it in the IDT.
*
* 2. **Fill all 256 IDT entries.**
* Calls @c set_idt_entry() for each vector with the corrected stub address,
* selector 0x08, IST 0, and type_attr @c INTERRUPT_GATE (0x8E).
*
* 3. **Load the IDT and mask the PIC.**
* Calls @c lidt() with the fully populated @c idt[] array, then calls
* @c pic_disable() to mask both PIC1 and PIC2 before @c STI is issued.
* This prevents spurious legacy PIC IRQs from reaching the IDT before the
* Local APIC takes over.
*
* Must be called after @c gdt_init() (for selector 0x08) and before
* @c apic_init() / @c apic_timer_init() / @c arch_enable_interrupts().
*/
void arch_interrupts_init(void)
{
uint64_t link_time_addr = (uint64_t)isr_stub_table[0];
uint64_t runtime_addr = (uint64_t)&isr_stub0;
int64_t reloc_offset = (int64_t)(runtime_addr - link_time_addr);
for (int i = 0; i < IDT_ENTRIES; ++i) {
void *isr_addr = (void *)((uint64_t)isr_stub_table[i] + (uint64_t)reloc_offset);
set_idt_entry(i, isr_addr);
}
struct idtr idtr_desc;
idtr_desc.limit = (uint16_t)(sizeof(idt) - 1u);
idtr_desc.base = (uint64_t)&idt[0];
lidt(&idtr_desc);
pic_disable();
}