746 lines
28 KiB
Zig
746 lines
28 KiB
Zig
//! x86_64 CPU operations. This is the "architecture" module: the generic kernel imports
|
|
//! it as `@import("architecture")` and never names x86_64 directly, so a second
|
|
//! architecture is added by pointing that module at a different directory in
|
|
//! build.zig — no change to the generic code. Keep everything CPU-specific here
|
|
//! (halt, the descriptor tables, later paging), and nothing generic.
|
|
|
|
const boot_handoff = @import("boot-handoff");
|
|
const parameters = @import("parameters");
|
|
const gdt = @import("gdt.zig");
|
|
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");
|
|
const ioapic = @import("ioapic.zig");
|
|
const io = @import("io.zig");
|
|
const smp = @import("smp.zig");
|
|
const pcpu = @import("per-cpu.zig");
|
|
|
|
/// The saved register/trap frame passed to a fault handler.
|
|
pub const CpuState = idt.CpuState;
|
|
|
|
// --- trap-frame accessors ---------------------------------------------------
|
|
// The frame's fields are x86_64 registers; the generic kernel reads it through
|
|
// these accessors so it never names one.
|
|
|
|
/// The interrupted/faulting instruction address (RIP here; ELR_EL1 on aarch64,
|
|
/// sepc on riscv64).
|
|
pub fn instructionPointer(state: *const CpuState) u64 {
|
|
return state.rip;
|
|
}
|
|
|
|
/// The interrupted stack pointer (RSP here).
|
|
pub fn stackPointer(state: *const CpuState) u64 {
|
|
return state.rsp;
|
|
}
|
|
|
|
/// Whether the trap came from user mode (CPL 3 here; EL0 on aarch64, U-mode on
|
|
/// riscv64).
|
|
pub fn fromUser(state: *const CpuState) bool {
|
|
return state.cs & 3 == 3;
|
|
}
|
|
|
|
/// The faulting virtual address, if this trap is a page fault (CR2 here;
|
|
/// FAR_EL1 on aarch64, stval on riscv64). Null for any other exception.
|
|
pub fn faultAddress(state: *const CpuState) ?u64 {
|
|
if (state.vector != 14) return null;
|
|
return asm volatile ("mov %%cr2, %[out]"
|
|
: [out] "=r" (-> u64),
|
|
);
|
|
}
|
|
|
|
// --- system_call ABI ------------------------------------------------------------
|
|
// The System V-style register convention (number in rax, arguments in
|
|
// rdi/rsi/rdx/r10/r8/r9, result in rax), exposed positionally so the generic
|
|
// dispatcher never names a register.
|
|
|
|
/// The system_call number the user program passed.
|
|
pub fn systemCallNumber(state: *const CpuState) u64 {
|
|
return state.rax;
|
|
}
|
|
|
|
/// Positional system_call argument `n`.
|
|
pub fn systemCallArg(state: *const CpuState, n: u8) u64 {
|
|
return switch (n) {
|
|
0 => state.rdi,
|
|
1 => state.rsi,
|
|
2 => state.rdx,
|
|
3 => state.r10,
|
|
4 => state.r8,
|
|
5 => state.r9,
|
|
else => 0,
|
|
};
|
|
}
|
|
|
|
/// Write the system_call's return value into the frame — the entry paths restore
|
|
/// user registers from it.
|
|
pub fn setSystemCallResult(state: *CpuState, value: u64) void {
|
|
state.rax = value;
|
|
}
|
|
|
|
/// Write a *second* system_call return value (rdx here — restored by both the
|
|
/// system_call/sysret and int-0x80 entry paths; unlike rcx/r11 it is not consumed by
|
|
/// sysretq). Used by IPC_ReplyWait to hand back the sender's badge alongside the
|
|
/// message length in rax.
|
|
pub fn setSystemCallResult2(state: *CpuState, value: u64) void {
|
|
state.rdx = value;
|
|
}
|
|
|
|
/// Write a *third* system_call return value (r8 here). r8 is an input argument
|
|
/// register (arg #4), but the syscall/int-0x80 stubs push and pop it around the
|
|
/// dispatch, so a value written into the frame is restored to the user on return.
|
|
/// Used by the IPC cap-passing calls to hand back the received capability handle.
|
|
pub fn setSystemCallResult3(state: *CpuState, value: u64) void {
|
|
state.r8 = value;
|
|
}
|
|
|
|
/// Bring up the serial port (the kernel's machine-readable log). No dependencies,
|
|
/// so it can be the very first thing called.
|
|
pub fn serialInit() void {
|
|
serial.init();
|
|
}
|
|
|
|
/// Write bytes to the serial port.
|
|
pub fn serialWrite(bytes: []const u8) void {
|
|
serial.write(bytes);
|
|
}
|
|
|
|
/// Whether a working UART was detected (loopback probe). When false the serial
|
|
/// sink is silently inert — a dead legacy COM1 costs nothing per byte.
|
|
pub fn serialPresent() bool {
|
|
return serial.present();
|
|
}
|
|
|
|
/// Emit a one-byte progress checkpoint to whatever hardware debug sink the
|
|
/// platform has — here the POST diagnostic port (0x80), which a POST card or BMC
|
|
/// displays. The last-resort progress signal when there's no text output at all.
|
|
/// Writing 0x80 is universally safe (it's the legacy I/O-delay port).
|
|
pub fn checkpoint(code: u8) void {
|
|
io.outb(0x80, code);
|
|
}
|
|
|
|
/// Whether a Bochs/QEMU-style debug console is on port 0xE9 (it returns 0xE9 when
|
|
/// read). On real hardware the port reads back 0xFF, so this stays false — a safe
|
|
/// probe before we write to it.
|
|
pub fn debugconPresent() bool {
|
|
return io.inb(0xE9) == 0xE9;
|
|
}
|
|
|
|
/// Output sink: write bytes to the 0xE9 debug console (see `debugconPresent`).
|
|
pub fn debugconWrite(bytes: []const u8) void {
|
|
for (bytes) |b| io.outb(0xE9, b);
|
|
}
|
|
|
|
/// Set up the CPU's descriptor tables: our own GDT, the TSS (with an interrupt
|
|
/// stack for double faults), then the IDT with exception handlers. After this a
|
|
/// CPU fault is reported instead of triple-faulting. Install the fault handler
|
|
/// (setFaultHandler) first so early faults are caught.
|
|
pub fn init() void {
|
|
gdt.init();
|
|
tss.init();
|
|
idt.init();
|
|
pcpu.initSystemCall();
|
|
}
|
|
|
|
/// Build the kernel's own page tables (with real permissions) and switch onto
|
|
/// them. Needs the frame allocator and the boot info (for the memory map and the
|
|
/// kernel's segment layout). Call once the frame allocator is up.
|
|
pub fn enablePaging(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const boot_handoff.BootInformation) void {
|
|
paging.init(allocFrame, freeFrame, boot_information);
|
|
}
|
|
|
|
/// Create a new address space (returns the physical address of its root table —
|
|
/// the PML4 here — or null). Shares the kernel's higher half; the user (low)
|
|
/// half starts empty.
|
|
pub fn createAddressSpace() ?u64 {
|
|
return paging.createAddressSpace();
|
|
}
|
|
|
|
/// Free an address space and everything mapped in its user half. Caller must not
|
|
/// be running on it.
|
|
pub fn destroyAddressSpace(root: u64) void {
|
|
paging.destroyAddressSpace(root);
|
|
}
|
|
|
|
/// Map a user page into address space `root` (W^X is the caller's contract).
|
|
pub fn mapUserPageInto(root: u64, virtual: u64, physical: u64, writable: bool, executable: bool) void {
|
|
paging.mapUserInto(root, virtual, physical, writable, executable);
|
|
}
|
|
|
|
/// Map a device MMIO window into address space `root`: RW+NX, and marked so teardown
|
|
/// won't free the MMIO frames as RAM. `write_combining` picks the cache type —
|
|
/// false = strong-uncacheable (registers), true = write-combining (a framebuffer).
|
|
/// For IO passthrough.
|
|
pub fn mapUserDeviceInto(root: u64, virtual: u64, physical: u64, len: u64, write_combining: bool) void {
|
|
paging.mapUserDeviceInto(root, virtual, physical, len, write_combining);
|
|
}
|
|
|
|
/// Is the user leaf mapping `virtual` in address space `root` write-combining? Null if
|
|
/// unmapped. For tests verifying the framebuffer map's cache type.
|
|
pub fn userLeafIsWriteCombining(root: u64, virtual: u64) ?bool {
|
|
return paging.leafIsWriteCombining(root, virtual);
|
|
}
|
|
|
|
/// Map coherent DMA RAM into address space `root`: strong-uncacheable, RW+NX, but
|
|
/// reclaimed on teardown (real RAM, not MMIO). For dma_alloc.
|
|
pub fn mapUserDmaInto(root: u64, virtual: u64, physical: u64, len: u64) void {
|
|
paging.mapUserDmaInto(root, virtual, physical, len);
|
|
}
|
|
|
|
/// Map shared cacheable RAM into address space `root`: write-back cacheable, RW+NX, and
|
|
/// marked so teardown won't free the frames (they're owned by a refcounted shm object,
|
|
/// freed when its last capability drops). For shm_create/shm_map.
|
|
pub fn mapUserSharedInto(root: u64, virtual: u64, physical: u64, len: u64) void {
|
|
paging.mapUserSharedInto(root, virtual, physical, len);
|
|
}
|
|
|
|
/// Map a page into the kernel address space (non-executable). For the heap, etc.
|
|
pub fn mapPage(virtual: u64, physical: u64, writable: bool) void {
|
|
paging.map(virtual, physical, writable);
|
|
}
|
|
|
|
/// Map a device MMIO range and return the virtual address to reach it at. This
|
|
/// is the device layer's `Hal.mapMmio` — it hands back a physmap pointer and
|
|
/// never exposes how the mapping is placed.
|
|
pub fn mapMmio(physical: u64, len: u64, writable: bool) u64 {
|
|
return paging.mapMmio(physical, len, writable);
|
|
}
|
|
|
|
/// The kernel's page-table root (physical), shared into every address space.
|
|
pub fn kernelPageTable() u64 {
|
|
return paging.kernelPml4();
|
|
}
|
|
|
|
/// Switch the active address space (load CR3 with a physical root table).
|
|
pub fn loadPageTable(root: u64) void {
|
|
paging.loadCr3(root);
|
|
}
|
|
|
|
/// The physical root of the currently active page tables (CR3 here; TTBR0/satp
|
|
/// elsewhere).
|
|
pub fn activePageTable() u64 {
|
|
return asm volatile ("mov %%cr3, %[out]"
|
|
: [out] "=r" (-> u64),
|
|
);
|
|
}
|
|
|
|
/// Set core `cpu`'s kernel stack pointer for ring-3 -> ring-0 transitions:
|
|
/// TSS.rsp0 (for interrupts/exceptions, which switch stacks in hardware) and the
|
|
/// per-CPU `kernel_rsp` (for the system_call stub, which switches by hand). Updated
|
|
/// by the scheduler when it switches to a user task.
|
|
pub fn setKernelStack(cpu: usize, top: usize) void {
|
|
tss.rsp0Ptr(cpu).* = top;
|
|
pcpu.setKernelRsp(cpu, top);
|
|
}
|
|
|
|
/// Remove a kernel mapping.
|
|
pub fn unmapPage(virtual: u64) void {
|
|
paging.unmap(virtual);
|
|
}
|
|
|
|
/// Remove a page mapping from address space `root` (for munmap of user pages).
|
|
/// Clears the leaf entry only; freeing the underlying frame is the caller's job.
|
|
pub fn unmapUserPageInto(root: u64, virtual: u64) void {
|
|
paging.unmapInto(root, virtual);
|
|
}
|
|
|
|
/// Resolve `virtual` to its physical address in the address space rooted at `root`
|
|
/// (any address space, not just the live one), or null if unmapped. Used to find
|
|
/// the frame behind a user page for munmap, and for cross-address-space copies.
|
|
pub fn translate(root: u64, virtual: u64) ?u64 {
|
|
return paging.translateIn(root, virtual);
|
|
}
|
|
|
|
/// Map a page accessible from ring 3 (U/S bit at every level). The caller keeps
|
|
/// W^X: code read-only + executable, data writable + no-execute.
|
|
pub fn mapUserPage(virtual: u64, physical: u64, writable: bool, executable: bool) void {
|
|
paging.mapUser(virtual, physical, writable, executable);
|
|
}
|
|
|
|
// --- ring 3 entry/exit -----------------------------------------------------
|
|
|
|
/// Drop to ring 3 at `rip` on `rsp` (defined in isr.s). Saves the kernel context,
|
|
/// publishes the kernel stack pointer through `rsp0_slot` (this core's TSS.rsp0,
|
|
/// so ring-3 interrupts land on a good stack), builds an iretq frame with the
|
|
/// user selectors, and iretq's. "Returns" only when the user program triggers
|
|
/// the exit path (user_exit_to_kernel).
|
|
extern fn enter_user(rip: u64, rsp: u64, rsp0_slot: *align(4) u64) callconv(.c) void;
|
|
|
|
/// Abandon the in-flight ring-3 trap context and resume the kernel as if
|
|
/// `enter_user` had returned (defined in isr.s). Called by the exit system_call.
|
|
extern fn user_exit_to_kernel() callconv(.c) noreturn;
|
|
|
|
/// Run user code at `entry` with stack `stack_top` on this core (`cpu` = the
|
|
/// caller's CPU index; the architecture layer can't ask the scheduler). Returns after the
|
|
/// user program exits via system_call. Interrupts are disabled on return (the exit
|
|
/// arrives through an interrupt gate) — the caller re-enables.
|
|
pub fn enterUser(cpu: usize, entry: u64, stack_top: u64) void {
|
|
enter_user(entry, stack_top, tss.rsp0Ptr(cpu));
|
|
}
|
|
|
|
/// Never returns to the user program: unwind to the kernel context that called
|
|
/// `enterUser`. For the exit system_call's handler.
|
|
pub fn userExit() noreturn {
|
|
user_exit_to_kernel();
|
|
}
|
|
|
|
/// Register the handler for the user system_call gate (int 0x80, vector 128). The
|
|
/// handler may write the trap frame (see `setSystemCallResult`).
|
|
pub fn setSystemCallHandler(handler: *const fn (*CpuState) void) void {
|
|
idt.setSystemCallHandler(handler);
|
|
}
|
|
|
|
/// Publish core `cpu`'s scheduler pointer via its per-CPU block (GS base). Each
|
|
/// core calls this once, after its GDT is in place (a GS *selector* reload would
|
|
/// clobber the base). See percpu.zig for the swapgs discipline.
|
|
pub fn setCpuLocal(cpu: usize, ptr: usize) void {
|
|
pcpu.setLocal(cpu, ptr);
|
|
}
|
|
|
|
/// This core's scheduler pointer (via the GS base) — a per-core register, so each
|
|
/// core sees its own without locking. Valid in any ring-0 context.
|
|
pub fn cpuLocal() usize {
|
|
return pcpu.scheduler();
|
|
}
|
|
|
|
// --- SMP: application-processor bring-up ----------------------------------
|
|
|
|
/// Record the low (<1 MiB) frame reserved for the AP trampoline. Run once at boot.
|
|
/// The frame stays inert (zeroed, non-executable) between wakes and is armed only
|
|
/// while a core is climbing — so a core can be (re)woken at any time (retry, or a
|
|
/// future power manager) without leaving an executable page resident. See smp.zig.
|
|
pub fn setTrampolinePage(physical: u64) void {
|
|
smp.setTrampolinePage(physical);
|
|
}
|
|
|
|
/// Wake the core with hardware id `hw_id` (its Local APIC id here; MPIDR on
|
|
/// aarch64, hart id on riscv64) as dense CPU `index`, giving it `stack_top` and
|
|
/// its per-CPU pointer `percpu`; it adopts the kernel page tables. Returns false
|
|
/// if it doesn't come online within the timeout. Blocks until the core reports in.
|
|
pub fn startSecondary(hw_id: u32, stack_top: usize, percpu: usize, index: usize) bool {
|
|
// The AP adopts the kernel page tables explicitly — never the caller's live
|
|
// CR3, which a future re-wake from a core running a process would make a
|
|
// process address space.
|
|
return smp.startAp(hw_id, stack_top, percpu, index, paging.kernelPml4());
|
|
}
|
|
|
|
/// Register the generic entry a woken AP jumps to once its architecture state is up (its own
|
|
/// descriptor tables, LAPIC, and timer). The kernel passes its scheduler entry here.
|
|
pub fn setSecondaryEntry(entry: *const fn () callconv(.c) noreturn) void {
|
|
smp.setSecondaryEntry(entry);
|
|
}
|
|
|
|
/// Bytes the kernel should allocate for a secondary core's dedicated fault stack
|
|
/// (the IST double-fault stack here), and where to record its top before waking
|
|
/// the core. The stack is heap-allocated per online core (the boot CPU's is
|
|
/// static — it's needed before the allocator exists). See tss.zig.
|
|
pub const fault_stack_size = tss.ist_stack_size;
|
|
pub fn setFaultStack(cpu: usize, top: usize) void {
|
|
tss.setApIstStack(cpu, top);
|
|
}
|
|
|
|
/// Test hook: force the next `n` AP wake attempts to fail, so the retry path can be
|
|
/// exercised deterministically (see the smp-retry test). No effect when `n` is 0.
|
|
pub fn testFailNextWakes(n: u32) void {
|
|
smp.testFailNextWakes(n);
|
|
}
|
|
|
|
/// The reserved AP-trampoline frame (0 if none). For tests that check it's inert.
|
|
pub fn trampolinePage() u64 {
|
|
return smp.trampolinePage();
|
|
}
|
|
|
|
/// Whether the page at `virtual` is currently mapped executable (present, NX clear).
|
|
pub fn pageExecutable(virtual: u64) bool {
|
|
return paging.isExecutable(virtual);
|
|
}
|
|
|
|
/// Kernel tick rate (the scheduler's time quantum), from configuration.
|
|
pub const timer_hz = parameters.timer_hz;
|
|
|
|
/// The ACPI PM timer, as a calibration reference (re-exported for the configuration).
|
|
pub const PmTimer = apic.PmTimer;
|
|
/// A MADT interrupt-source override (re-exported for the configuration).
|
|
pub const IsoEntry = ioapic.IsoEntry;
|
|
|
|
/// Discovered platform facts the architecture layer needs so it makes no legacy
|
|
/// assumptions — sourced from the device tree + ACPI, passed in by the kernel.
|
|
pub const PlatformConfiguration = struct {
|
|
/// Whether the legacy 8259 PIC is present (skip programming it if not).
|
|
pic_present: bool = true,
|
|
/// HPET MMIO base (0 = none) — a calibration reference for the timer.
|
|
hpet_base: u64 = 0,
|
|
/// The ACPI PM timer, another calibration reference.
|
|
pm_timer: ?PmTimer = null,
|
|
/// I/O APIC MMIO base + its first global system interrupt (0 = none).
|
|
ioapic_base: u64 = 0,
|
|
ioapic_gsi_base: u32 = 0,
|
|
/// MADT ISA-IRQ overrides, for I/O APIC routing.
|
|
overrides: []const IsoEntry = &.{},
|
|
};
|
|
|
|
/// Apply the discovered platform configuration. Must run before `startTimer` (the timer
|
|
/// calibration reads `hpet_base`/`pm_timer`) and before any interrupt routing.
|
|
/// Maps + masks the I/O APIC immediately.
|
|
pub fn configurePlatform(configuration: PlatformConfiguration) void {
|
|
apic.configure(configuration.pic_present, configuration.hpet_base, configuration.pm_timer);
|
|
ioapic.configure(configuration.ioapic_base, configuration.ioapic_gsi_base, configuration.overrides);
|
|
ioapic.init();
|
|
}
|
|
|
|
/// Point the serial console at the UART ACPI's SPCR table named (MMIO or I/O port).
|
|
pub fn serialReconfigure(is_mmio: bool, address: u64) void {
|
|
serial.reconfigure(is_mmio, address);
|
|
}
|
|
|
|
/// The reference clock the timer was calibrated against ("cpuid"/"hpet"/…).
|
|
pub fn timerCalibrationSource() []const u8 {
|
|
return apic.calibrationSource();
|
|
}
|
|
|
|
/// External-interrupt-router diagnostics, for boot logging / verification (the
|
|
/// I/O APIC's redirection entries here; a GIC distributor or PLIC elsewhere).
|
|
pub fn irqRouteCount() u32 {
|
|
return ioapic.entryCount();
|
|
}
|
|
pub fn irqRouteRaw(n: u32) u32 {
|
|
return ioapic.entryLow(n);
|
|
}
|
|
|
|
// --- device-IRQ plumbing, for system/kernel/irq.zig -----------------------------
|
|
//
|
|
// The generic IRQ layer speaks GSIs and vectors; everything below hides the fact
|
|
// that on x86_64 those mean "I/O APIC redirection entry" and "IDT gate". The
|
|
// vector window is bounded by the stubs isr.s actually emits: `gate_count` = 48,
|
|
// vector 32 is the LAPIC timer and 47 is the spurious vector, leaving 33..46.
|
|
|
|
pub const irq_vector_base: u8 = 33;
|
|
pub const irq_vector_count: u8 = 14; // 33..46 inclusive
|
|
|
|
/// True if `gsi` is one this machine's interrupt router can deliver.
|
|
pub fn irqOwnsGsi(gsi: u32) bool {
|
|
return ioapic.ownsGsi(gsi);
|
|
}
|
|
|
|
/// Install `handler` on `vector` (an absolute IDT gate index).
|
|
pub fn irqSetHandler(vector: u8, handler: *const fn () void) void {
|
|
idt.setHandler(vector, handler);
|
|
}
|
|
|
|
/// Route `gsi` to `vector` on *this* core, masked. Unmask with `irqUnmask` once bound.
|
|
pub fn irqRoute(gsi: u32, vector: u8, level: bool, active_low: bool) void {
|
|
ioapic.routeGsi(gsi, vector, apic.localId(), level, active_low);
|
|
}
|
|
|
|
pub fn irqMask(gsi: u32) void {
|
|
ioapic.maskGsi(gsi);
|
|
}
|
|
pub fn irqUnmask(gsi: u32) void {
|
|
ioapic.unmaskGsi(gsi);
|
|
}
|
|
|
|
/// Acknowledge the interrupt currently in service on this core's LAPIC.
|
|
pub fn irqEoi() void {
|
|
apic.eoi();
|
|
}
|
|
|
|
/// Send this core a fixed interrupt at `vector`. Stands in for a device's MSI write
|
|
/// so the MSI vector-routing path can be exercised without MSI-capable hardware.
|
|
pub fn selfIpi(vector: u8) void {
|
|
apic.selfIpi(vector);
|
|
}
|
|
|
|
/// Enable the Local APIC, calibrate its timer against the best available reference
|
|
/// (see apic.calibrate — no longer the PIT by default), and start it firing at
|
|
/// `timer_hz` — the kernel's real-time heartbeat. Interrupts still have to be
|
|
/// unmasked with enableInterrupts() to be delivered. Run `configurePlatform` first.
|
|
pub fn startTimer() void {
|
|
apic.init();
|
|
apic.calibrate();
|
|
idt.setHandler(apic.timer_vector, apic.timerTick);
|
|
apic.initTimer(timer_hz);
|
|
}
|
|
|
|
/// Number of timer ticks since startTimer().
|
|
pub fn ticks() u64 {
|
|
return apic.ticks();
|
|
}
|
|
|
|
// Monotonic high-resolution clock (from the TSC), one function per resolution.
|
|
pub fn nanos() u64 {
|
|
return apic.nanos();
|
|
}
|
|
pub fn micros() u64 {
|
|
return apic.micros();
|
|
}
|
|
pub fn millis() u64 {
|
|
return apic.millis();
|
|
}
|
|
|
|
/// Measured frequency of the tick timer's input clock (the LAPIC timer here), in
|
|
/// Hz, from calibration.
|
|
pub fn timerClockHz() u64 {
|
|
return apic.lapicHz();
|
|
}
|
|
|
|
/// Measured frequency of the monotonic clock's underlying counter (the TSC here;
|
|
/// CNTVCT on aarch64, `time` on riscv64), in Hz.
|
|
pub fn clockHz() u64 {
|
|
return apic.tscHz();
|
|
}
|
|
|
|
// --- real-time clock (CMOS) --------------------------------------------------
|
|
//
|
|
// The battery-backed CMOS clock, read once at boot and thereafter anchored to the
|
|
// monotonic clock (see kernel/wall-clock.zig) — so this is never on a hot path and
|
|
// needs no lock. Wall-clock *seconds* are mechanism the kernel owns (the hardware's
|
|
// value), like the monotonic clock; calendars/timezones are policy layered on top.
|
|
|
|
fn cmosRead(register: u8) u8 {
|
|
io.outb(0x70, register);
|
|
return io.inb(0x71);
|
|
}
|
|
|
|
const RtcFields = struct { second: u8, minute: u8, hour: u8, day: u8, month: u8, year: u8 };
|
|
|
|
fn rtcRaw() RtcFields {
|
|
while (cmosRead(0x0A) & 0x80 != 0) {} // wait out any update in progress (status A bit 7)
|
|
return .{
|
|
.second = cmosRead(0x00),
|
|
.minute = cmosRead(0x02),
|
|
.hour = cmosRead(0x04),
|
|
.day = cmosRead(0x07),
|
|
.month = cmosRead(0x08),
|
|
.year = cmosRead(0x09),
|
|
};
|
|
}
|
|
|
|
fn bcdToBinary(v: u8) u8 {
|
|
return (v & 0x0F) + ((v >> 4) * 10);
|
|
}
|
|
|
|
fn isLeapYear(y: u32) bool {
|
|
return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0);
|
|
}
|
|
|
|
/// Read the CMOS real-time clock and convert it to Unix epoch seconds (UTC).
|
|
pub fn readRtcUnixSeconds() u64 {
|
|
// Read until two consecutive reads agree, so we never latch a half-updated time.
|
|
var a = rtcRaw();
|
|
while (true) {
|
|
const b = rtcRaw();
|
|
if (a.second == b.second and a.minute == b.minute and a.hour == b.hour and
|
|
a.day == b.day and a.month == b.month and a.year == b.year) break;
|
|
a = b;
|
|
}
|
|
|
|
const status_b = cmosRead(0x0B);
|
|
const binary_mode = status_b & 0x04 != 0; // else BCD
|
|
const hour_24 = status_b & 0x02 != 0; // else 12-hour with a PM bit
|
|
|
|
var second = a.second;
|
|
var minute = a.minute;
|
|
var hour_field = a.hour;
|
|
var day = a.day;
|
|
var month = a.month;
|
|
var year = a.year;
|
|
if (!binary_mode) {
|
|
second = bcdToBinary(second);
|
|
minute = bcdToBinary(minute);
|
|
hour_field = bcdToBinary(hour_field & 0x7F) | (hour_field & 0x80); // preserve the PM bit
|
|
day = bcdToBinary(day);
|
|
month = bcdToBinary(month);
|
|
year = bcdToBinary(year);
|
|
}
|
|
|
|
var hour: u32 = hour_field & 0x7F;
|
|
if (!hour_24) {
|
|
const pm = hour_field & 0x80 != 0;
|
|
hour %= 12; // 12 AM/PM -> 0
|
|
if (pm) hour += 12;
|
|
}
|
|
|
|
// The CMOS year is 0..99; QEMU and modern hardware mean 20xx (there is no
|
|
// reliable century register on QEMU). Treat < 70 as 20xx, else 19xx.
|
|
const full_year: u32 = if (year < 70) 2000 + @as(u32, year) else 1900 + @as(u32, year);
|
|
|
|
var days: u64 = 0;
|
|
var y: u32 = 1970;
|
|
while (y < full_year) : (y += 1) days += if (isLeapYear(y)) 366 else 365;
|
|
const month_lengths = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
|
var m: u8 = 1;
|
|
while (m < month) : (m += 1) {
|
|
days += month_lengths[m - 1];
|
|
if (m == 2 and isLeapYear(full_year)) days += 1;
|
|
}
|
|
days += @as(u64, day) - 1;
|
|
|
|
return ((days * 24 + hour) * 60 + minute) * 60 + second;
|
|
}
|
|
|
|
/// Whether the CPU guarantees an **invariant** TSC (CPUID 0x80000007 EDX[8] on
|
|
/// x86; the analogous architectural guarantee elsewhere). When false the TSC is not
|
|
/// used as the clocksource.
|
|
pub fn clockInvariant() bool {
|
|
return apic.tscInvariant();
|
|
}
|
|
|
|
/// Whether the per-core clock counters are synchronized (no backward warp observed
|
|
/// at SMP bring-up). When false the clock falls back off the TSC.
|
|
pub fn clockSynchronized() bool {
|
|
return apic.tscSynced();
|
|
}
|
|
|
|
/// The active monotonic clocksource, for the boot log ("tsc" or "hpet" on x86).
|
|
pub fn clockSourceName() []const u8 {
|
|
return apic.clockSourceName();
|
|
}
|
|
|
|
/// Test hook: force the TSC clocksource on, to exercise the TSC + warp-check path on
|
|
/// a hypervisor that won't advertise an invariant TSC (see apic.forceTscClocksourceForTest).
|
|
pub fn forceTscClocksourceForTest() void {
|
|
apic.forceTscClocksourceForTest();
|
|
}
|
|
|
|
/// How many per-AP TSC warp checks completed (for the tsc-sync test).
|
|
pub fn warpChecksRun() u32 {
|
|
return apic.warpChecksRun();
|
|
}
|
|
|
|
/// 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");
|
|
}
|
|
|
|
/// Disable interrupts and return the previous flags, so a nested critical section
|
|
/// can restore the caller's state rather than blindly re-enabling. Pairs with
|
|
/// restoreInterrupts.
|
|
pub fn saveInterrupts() u64 {
|
|
var flags: u64 = undefined;
|
|
asm volatile (
|
|
\\pushfq
|
|
\\pop %[f]
|
|
\\cli
|
|
: [f] "=r" (flags),
|
|
:
|
|
: .{ .memory = true });
|
|
return flags;
|
|
}
|
|
|
|
/// Re-enable interrupts only if they were enabled when `flags` was captured.
|
|
pub fn restoreInterrupts(flags: u64) void {
|
|
if (flags & 0x200 != 0) asm volatile ("sti" ::: .{ .memory = true }); // bit 9 = IF
|
|
}
|
|
|
|
/// Register a callback the timer interrupt invokes each tick (e.g. the scheduler).
|
|
pub fn setTickHook(hook: *const fn () void) void {
|
|
apic.setTickHook(hook);
|
|
}
|
|
|
|
// --- context switching (for the scheduler) -------------------------------
|
|
|
|
/// Save the current task's registers/stack and resume `new_rsp`; the old stack
|
|
/// pointer is written to `old_rsp`. Defined in isr.s.
|
|
extern fn switch_context(old_rsp: *usize, new_rsp: usize) callconv(.c) void;
|
|
|
|
pub fn switchContext(old_sp: *usize, new_sp: usize) void {
|
|
switch_context(old_sp, new_sp);
|
|
}
|
|
|
|
/// Build the initial stack for a new task so that switching to it lands in
|
|
/// `task_trampoline`, which then calls `entry`. Returns the saved stack pointer.
|
|
/// The layout must match switch_context's push order (callee-saved, then the
|
|
/// return address on top); `entry` is smuggled in via the r15 slot.
|
|
pub fn initTaskStack(stack_top: usize, entry: usize) usize {
|
|
const trampoline = @extern(*const anyopaque, .{ .name = "task_trampoline" });
|
|
var sp = stack_top;
|
|
const push = struct {
|
|
fn f(p: *usize, value: usize) void {
|
|
p.* -= @sizeOf(usize);
|
|
@as(*usize, @ptrFromInt(p.*)).* = value;
|
|
}
|
|
}.f;
|
|
push(&sp, @intFromPtr(trampoline)); // return address for switch_context's `ret`
|
|
push(&sp, 0); // rbx
|
|
push(&sp, 0); // rbp
|
|
push(&sp, 0); // r12
|
|
push(&sp, 0); // r13
|
|
push(&sp, 0); // r14
|
|
push(&sp, entry); // r15 -> task entry, read by task_trampoline
|
|
return sp;
|
|
}
|
|
|
|
/// Drop the current (kernel-context) task to ring 3 at `rip` on `rsp`, never
|
|
/// returning (defined in isr.s). Used by the scheduler's user-task trampoline
|
|
/// once it has switched onto the task and read its entry/stack. Interrupts are
|
|
/// disabled across the swapgs+iretq so no interrupt observes the user GS base in
|
|
/// ring 0; the pushed RFLAGS re-enables them in ring 3.
|
|
extern fn jump_to_user(rip: u64, rsp: u64) callconv(.c) noreturn;
|
|
|
|
pub fn jumpToUser(entry: u64, stack_top: u64) noreturn {
|
|
jump_to_user(entry, stack_top);
|
|
}
|
|
|
|
/// As `jumpToUser`, but delivers `arg0` in the user's `rdi` — how a fresh thread
|
|
/// receives its closure pointer (docs/threading.md). A normal process is dropped
|
|
/// with `arg0 = 0`, which its `_start` ignores (it reads argv off the stack).
|
|
extern fn jump_to_user_arg(rip: u64, rsp: u64, arg0: u64) callconv(.c) noreturn;
|
|
|
|
pub fn jumpToUserArg(entry: u64, stack_top: u64, arg0: u64) noreturn {
|
|
jump_to_user_arg(entry, stack_top, arg0);
|
|
}
|
|
|
|
/// 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 {
|
|
idt.on_fault = handler;
|
|
}
|
|
|
|
/// A human-readable name for a CPU exception vector.
|
|
pub fn exceptionName(vector: u64) []const u8 {
|
|
return idt.vectorName(vector);
|
|
}
|
|
|
|
/// Read `width` bytes (1/2/4) from an I/O port. The generic device layer drives
|
|
/// ACPI registers through this rather than naming x86 port instructions; on an
|
|
/// MMIO-only architecture this would be implemented differently.
|
|
pub fn pioRead(width: u8, port: u16) u32 {
|
|
return switch (width) {
|
|
1 => io.inb(port),
|
|
2 => io.inw(port),
|
|
4 => io.inl(port),
|
|
else => 0,
|
|
};
|
|
}
|
|
|
|
/// Write `width` bytes (1/2/4) to an I/O port.
|
|
pub fn pioWrite(width: u8, port: u16, value: u32) void {
|
|
switch (width) {
|
|
1 => io.outb(port, @truncate(value)),
|
|
2 => io.outw(port, @truncate(value)),
|
|
4 => io.outl(port, value),
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
/// Park the core forever. `hlt` drops it into a low-power idle until the next
|
|
/// interrupt; the loop re-halts on every wake so the stop is permanent. See
|
|
/// docs/halting.md for the full reasoning.
|
|
pub fn halt() noreturn {
|
|
while (true) asm volatile ("hlt");
|
|
}
|
|
|
|
/// Spin-wait hint (`pause`). Emitted in the body of a spinlock's busy-wait: it
|
|
/// relaxes the core while it polls a contended lock — yielding pipeline resources
|
|
/// to a hyperthread sibling and easing the cache-coherency traffic on the lock
|
|
/// line. Purely a performance/power hint; correct to omit, but kinder on the bus.
|
|
pub fn cpuRelax() void {
|
|
asm volatile ("pause");
|
|
}
|