Built device interrupts

This commit is contained in:
2026-07-03 12:57:40 +01:00
parent c8e89e8115
commit 5ea521d054
12 changed files with 367 additions and 23 deletions
+91
View File
@@ -0,0 +1,91 @@
//! Local APIC and its timer — the source of device interrupts.
//!
//! Modern x86 routes interrupts through the per-CPU Local APIC (the legacy 8259
//! PIC is remapped out of the way and masked). The LAPIC also has a built-in
//! timer, which is the simplest device interrupt to bring up: it needs no
//! external routing, just a vector and a count. We use it as danos's heartbeat.
//!
//! The LAPIC is memory-mapped (default physical 0xFEE00000, inside our identity
//! map). Every interrupt must be acknowledged with an end-of-interrupt write, or
//! the LAPIC won't deliver the next one.
const io = @import("io.zig");
/// IDT vector the timer fires on (in the device range, >= 32).
pub const timer_vector = 32;
/// Spurious-interrupt vector. Low nibble 0xF by convention; also in our gate
/// range so a stray spurious interrupt lands on a valid (no-op) handler.
const spurious_vector = 47;
// LAPIC register offsets.
const reg_spurious = 0x0F0;
const reg_eoi = 0x0B0;
const reg_lvt_timer = 0x320;
const reg_timer_initial = 0x380;
const reg_timer_divide = 0x3E0;
const ia32_apic_base_msr = 0x1B;
/// LAPIC MMIO base. A runtime var (not a constant) both because we read it from
/// the MSR and so register writes compile to normal stores rather than a
/// `mov moffs`, which the self-hosted backend can't encode.
var base: usize = 0xFEE00000;
var tick_count: u64 = 0;
fn read(reg: u32) u32 {
return @as(*volatile u32, @ptrFromInt(base + reg)).*;
}
fn write(reg: u32, value: u32) void {
@as(*volatile u32, @ptrFromInt(base + reg)).* = value;
}
/// Move the legacy 8259 PIC's vectors to 0x20-0x2F (clear of the CPU exception
/// vectors) and mask every line, so it can't deliver interrupts behind the APIC.
fn remapAndMaskPic() void {
io.outb(0x20, 0x11); // start init (cascade mode)
io.outb(0xA0, 0x11);
io.outb(0x21, 0x20); // master offset 0x20
io.outb(0xA1, 0x28); // slave offset 0x28
io.outb(0x21, 0x04); // tell master about slave on IRQ2
io.outb(0xA1, 0x02);
io.outb(0x21, 0x01); // 8086 mode
io.outb(0xA1, 0x01);
io.outb(0x21, 0xFF); // mask all
io.outb(0xA1, 0xFF);
}
/// Enable the Local APIC: mask the PIC, set the global-enable MSR bit, and
/// software-enable the APIC via its spurious-vector register.
pub fn init() void {
remapAndMaskPic();
const msr = io.rdmsr(ia32_apic_base_msr);
base = @intCast(msr & 0xFFFFF000); // physical base is bits 12+
io.wrmsr(ia32_apic_base_msr, msr | (1 << 11)); // global enable
write(reg_spurious, 0x100 | spurious_vector); // bit 8 = software enable
}
/// Arm the LAPIC timer in periodic mode on `timer_vector`.
pub fn initTimer() void {
write(reg_timer_divide, 0x3); // divide bus clock by 16
write(reg_lvt_timer, timer_vector | (1 << 17)); // periodic mode
write(reg_timer_initial, 1_000_000); // reload count -> periodic ticks
}
/// Acknowledge the current interrupt so the LAPIC will deliver the next one.
pub fn eoi() void {
write(reg_eoi, 0);
}
/// The timer interrupt handler: just count ticks for now.
pub fn timerTick() void {
tick_count +%= 1;
}
/// Number of timer ticks so far. Volatile load: the count is bumped
/// asynchronously by the interrupt handler, so callers must re-read memory.
pub fn ticks() u64 {
return @as(*const volatile u64, &tick_count).*;
}
+24
View File
@@ -9,6 +9,7 @@ const tss = @import("tss.zig");
const idt = @import("idt.zig");
const paging = @import("paging.zig");
const serial = @import("serial.zig");
const apic = @import("apic.zig");
/// The saved register/trap frame passed to a fault handler.
pub const CpuState = idt.CpuState;
@@ -47,6 +48,29 @@ pub fn readCr3() u64 {
);
}
/// Enable the Local APIC and start its periodic timer, the kernel's heartbeat.
/// Interrupts still have to be unmasked with enableInterrupts() to be delivered.
pub fn startTimer() void {
apic.init();
idt.setHandler(apic.timer_vector, apic.timerTick);
apic.initTimer();
}
/// Number of timer ticks since startTimer().
pub fn ticks() u64 {
return apic.ticks();
}
/// Unmask maskable interrupts (`sti`) so device interrupts get delivered.
pub fn enableInterrupts() void {
asm volatile ("sti");
}
/// Mask maskable interrupts (`cli`).
pub fn disableInterrupts() void {
asm volatile ("cli");
}
/// Route CPU exceptions to `handler`, which receives the trap frame and does not
/// return. Until set, faults just halt the core.
pub fn setFaultHandler(handler: *const fn (*const CpuState) noreturn) void {
+36 -11
View File
@@ -1,13 +1,30 @@
//! Interrupt Descriptor Table and the CPU-exception handlers. Without this, any
//! fault (a stray pointer, a bad page-table entry) triple-faults and silently
//! resets the machine. With it, the CPU vectors into our stubs, which capture the
//! register state and hand it to a reporter that prints what went wrong.
//! Interrupt Descriptor Table, CPU-exception handlers, and device-interrupt
//! dispatch. Without this, any fault (a stray pointer, a bad page-table entry)
//! triple-faults and silently resets the machine. With it, the CPU vectors into
//! our stubs, which capture the register state and hand it to a dispatcher.
//!
//! Only the 32 architecture-defined exception vectors are wired up here; device
//! interrupts (the APIC, timer, keyboard) come later.
//! Vectors split in two: 0-31 are CPU exceptions (terminal — reported and
//! halted); 32+ are device interrupts (a registered handler runs, the APIC is
//! acknowledged, and we return to the interrupted code).
const gdt = @import("gdt.zig");
const tss = @import("tss.zig");
const apic = @import("apic.zig");
/// Highest vector we install a gate/stub for (exceptions 0-31 plus the device
/// range 32-47, which covers the timer and the spurious vector).
const gate_count = 48;
/// A device-interrupt handler. It doesn't get the trap frame (a timer or keyboard
/// handler doesn't need the interrupted registers); add that if one ever does.
pub const Handler = *const fn () void;
var handlers = [_]?Handler{null} ** 256;
/// Register `handler` for a device-interrupt `vector` (>= 32).
pub fn setHandler(vector: usize, handler: Handler) void {
handlers[vector] = handler;
}
/// The register + trap frame the ISR stubs build on the stack, laid out so the
/// lowest address (where RSP points when we call the handler) is the first field.
@@ -101,9 +118,10 @@ fn setGate(vector: usize, handler: u64) void {
};
}
/// Point the first 32 vectors at the stubs defined in isr.s and load the IDT.
/// Point every installed vector at its stub (isr.s) and load the IDT.
pub fn init() void {
inline for (0..32) |vector| {
@setEvalBranchQuota(20000); // comptimePrint across all the gates adds up
inline for (0..gate_count) |vector| {
const stub = @extern(*const anyopaque, .{ .name = std.fmt.comptimePrint("isr{d}", .{vector}) });
setGate(vector, @intFromPtr(stub));
}
@@ -118,9 +136,16 @@ pub fn init() void {
}
/// Called by isr_common (isr.s) with a pointer to the trap frame. Exported so the
/// assembly stubs can `call` it by name.
export fn exceptionHandler(state: *const CpuState) callconv(.c) void {
on_fault(state);
/// assembly stubs can `call` it by name. Exceptions are terminal; device
/// interrupts run their handler, get acknowledged, and return.
export fn interruptDispatch(state: *const CpuState) callconv(.c) void {
if (state.vector < 32) {
on_fault(state); // CPU exception — never returns
} else if (handlers[state.vector]) |handler| {
handler();
apic.eoi();
}
// else: spurious/unhandled device interrupt — don't acknowledge it
}
const std = @import("std");
+38
View File
@@ -0,0 +1,38 @@
//! x86 port I/O and model-specific registers — the low-level primitives the
//! serial port and the APIC talk to hardware through.
pub fn outb(port: u16, value: u8) void {
asm volatile ("outb %[value], %[port]"
:
: [value] "{al}" (value),
[port] "{dx}" (port),
);
}
pub fn inb(port: u16) u8 {
return asm volatile ("inb %[port], %[value]"
: [value] "={al}" (-> u8),
: [port] "{dx}" (port),
);
}
/// Read a model-specific register (returns edx:eax combined).
pub fn rdmsr(msr: u32) u64 {
var low: u32 = undefined;
var high: u32 = undefined;
asm volatile ("rdmsr"
: [low] "={eax}" (low),
[high] "={edx}" (high),
: [msr] "{ecx}" (msr),
);
return (@as(u64, high) << 32) | low;
}
pub fn wrmsr(msr: u32, value: u64) void {
asm volatile ("wrmsr"
:
: [msr] "{ecx}" (msr),
[low] "{eax}" (@as(u32, @truncate(value))),
[high] "{edx}" (@as(u32, @truncate(value >> 32))),
);
}
+21 -2
View File
@@ -89,7 +89,26 @@ STUB_NOERR 29
STUB_NOERR 30
STUB_NOERR 31
.extern exceptionHandler
# Device-interrupt vectors (timer, spurious, room for more). None push an error
# code, so they all use the dummy-zero form.
STUB_NOERR 32
STUB_NOERR 33
STUB_NOERR 34
STUB_NOERR 35
STUB_NOERR 36
STUB_NOERR 37
STUB_NOERR 38
STUB_NOERR 39
STUB_NOERR 40
STUB_NOERR 41
STUB_NOERR 42
STUB_NOERR 43
STUB_NOERR 44
STUB_NOERR 45
STUB_NOERR 46
STUB_NOERR 47
.extern interruptDispatch
# Shared tail. Register push order here defines the CpuState field order.
isr_common:
@@ -109,7 +128,7 @@ isr_common:
push %r14
push %r15
mov %rsp, %rdi # first argument: pointer to the trap frame
call exceptionHandler
call interruptDispatch
pop %r15
pop %r14
pop %r13
+5
View File
@@ -91,6 +91,11 @@ fn kmain(boot_info: *const BootInfo) noreturn {
con.print("\ndanos: paging enabled\n", .{});
con.print(" page tables: CR3 = 0x{x:0>16}\n", .{arch.readCr3()});
// Start the timer and unmask interrupts — the kernel now has a heartbeat.
arch.startTimer();
arch.enableInterrupts();
con.write("\ndanos: timer interrupts enabled\n");
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
// Normal builds fall through to the idle halt.
if (build_options.test_case) |case| {
+23
View File
@@ -36,6 +36,8 @@ fn check(name: []const u8, ok: bool) void {
pub fn run(case: []const u8, boot_info: *const BootInfo) void {
if (eql(case, "smoke")) {
smoke(boot_info);
} else if (eql(case, "timer")) {
timer();
} else if (eql(case, "fault-ud")) {
faultInvalidOpcode();
} else if (eql(case, "fault-pf")) {
@@ -91,6 +93,26 @@ fn smoke(boot_info: *const BootInfo) void {
log("DANOS-TEST-DONE\n", .{});
}
/// Verify device interrupts fire and return: the timer tick counter must advance
/// on its own. Interrupts are already enabled by kmain before tests run.
fn timer() void {
log("DANOS-TEST-BEGIN: timer\n", .{});
const start = arch.ticks();
// Busy-wait for the counter to advance. arch.ticks() is a volatile load, so
// the compiler re-reads it each iteration and sees the interrupt's update.
// The cap is only a safety net; the harness timeout is the real backstop.
var spins: u64 = 0;
while (arch.ticks() == start and spins < 5_000_000_000) spins +%= 1;
check("timer interrupts advance the tick count", arch.ticks() > start);
log("DANOS-TEST-RESULT: {s} ({d} passed, {d} failed)\n", .{
if (failed == 0) "PASS" else "FAIL",
passed,
failed,
});
log("DANOS-TEST-DONE\n", .{});
}
fn faultInvalidOpcode() void {
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
asm volatile ("ud2");
@@ -108,6 +130,7 @@ fn faultPageFault() void {
fn faultDoubleFault() void {
log("DANOS-TEST-BEGIN: fault-df\n", .{});
arch.disableInterrupts(); // so only the ud2 delivery (not a timer tick) triggers the #DF
// Point RSP at unmapped memory, then fault: the CPU can't push the fault
// frame, which escalates to #DF — survivable only because #DF runs on IST1.
var bad_sp: u64 = 0x5000000000;