Built device interrupts
This commit is contained in:
@@ -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).*;
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user