From cb63d2e31bee79ec3de1e92239f21ec72339fb81 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:40:22 +0100 Subject: [PATCH] add multi-sink diagnostic log; make framebuffer optional --- src/boot/efi.zig | 10 +- src/kernel/arch/x86_64/cpu.zig | 19 ++++ src/kernel/console.zig | 76 ++++++++------ src/kernel/log.zig | 83 +++++++++++++++ src/kernel/main.zig | 186 ++++++++++++++++++++------------- src/root.zig | 11 +- 6 files changed, 279 insertions(+), 106 deletions(-) create mode 100644 src/kernel/log.zig diff --git a/src/boot/efi.zig b/src/boot/efi.zig index e8c9915..8f30d1c 100644 --- a/src/boot/efi.zig +++ b/src/boot/efi.zig @@ -34,7 +34,15 @@ fn boot() !noreturn { // Everything the kernel needs must be gathered *before* we exit boot // services, since afterwards none of these calls are usable. var boot_info: BootInfo = .{ - .framebuffer = try queryFramebuffer(bs), + // A missing GOP (a headless machine) is not fatal — hand the kernel a + // "no framebuffer" descriptor (base 0) and let it log to serial instead. + .framebuffer = queryFramebuffer(bs) catch danos.Framebuffer{ + .base = 0, + .width = 0, + .height = 0, + .pitch = 0, + .format = .bgrx, + }, .memory_map = undefined, // filled by exitBootServices, just below .kernel_segments = undefined, // filled by loadKernel .kernel_segment_count = 0, diff --git a/src/kernel/arch/x86_64/cpu.zig b/src/kernel/arch/x86_64/cpu.zig index e2989c3..9ef6385 100644 --- a/src/kernel/arch/x86_64/cpu.zig +++ b/src/kernel/arch/x86_64/cpu.zig @@ -28,6 +28,25 @@ pub fn serialWrite(bytes: []const u8) void { serial.write(bytes); } +/// Emit a one-byte checkpoint to the POST diagnostic port (0x80). A POST card or +/// BMC displays it; it's 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 postCode(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 diff --git a/src/kernel/console.zig b/src/kernel/console.zig index 6f3838d..149fdfe 100644 --- a/src/kernel/console.zig +++ b/src/kernel/console.zig @@ -1,10 +1,52 @@ //! A framebuffer text console: draws glyphs from an embedded PSF2 font directly //! into the linear framebuffer the bootloader handed us. No firmware, no driver -//! — just pixels. This is the kernel's first output device. +//! — just pixels. +//! +//! This is a **bootstrap** console — a stop-gap so early boot has something on +//! screen. The framebuffer is a general graphics surface, *not* inherently a text +//! terminal; once the driver machinery exists it becomes a proper graphics device +//! driver and this text-grid crutch goes away. It is therefore kept **separate +//! from the diagnostic [log](log.zig)** — the log fans out to serial/debugcon/file, +//! while this only paints the handful of user-facing status lines and panics. +//! +//! The module owns a single console and a `present` flag; `write` is a no-op when +//! the firmware handed over no framebuffer (a headless machine), so the kernel +//! never assumes a display exists. const std = @import("std"); const danos = @import("danos"); -const arch = @import("arch"); + +/// The one framebuffer console, valid only when `con_present`. +var con: Console = undefined; +var con_present: bool = false; + +/// Set up the console over `fb`, or mark it absent if there's no usable +/// framebuffer. Clears the screen when present. +pub fn init(fb: danos.Framebuffer) void { + if (!fb.present()) { + con_present = false; + return; + } + con = Console.init(fb); + if (con.cols == 0 or con.rows == 0) { + con_present = false; + return; + } + con.clear(); + con_present = true; +} + +/// Whether an on-screen console is available. +pub fn present() bool { + return con_present; +} + +/// Output sink: draw `bytes` on screen. A no-op when no framebuffer is present, +/// so it's always safe to call. +pub fn write(bytes: []const u8) void { + if (!con_present) return; + for (bytes) |c| con.putChar(c); +} /// The console font, embedded at compile time. cp850-8x16, PSF2 format: /// a 32-byte header, then 256 glyphs of 16 bytes each (one byte per 8-pixel @@ -40,19 +82,6 @@ pub const Console = struct { self.row = 0; } - pub fn write(self: *Console, bytes: []const u8) void { - // Mirror everything to the serial port so it's captured in logs / tests. - arch.serialWrite(bytes); - for (bytes) |c| self.putChar(c); - } - - /// Formatted output, e.g. `con.print("x={d}\n", .{x})`. Silently truncates - /// past 256 bytes — this is a debug console, not a general writer. - pub fn print(self: *Console, comptime fmt: []const u8, args: anytype) void { - var buf: [256]u8 = undefined; - self.write(std.fmt.bufPrint(&buf, fmt, args) catch return); - } - pub fn putChar(self: *Console, ch: u8) void { switch (ch) { '\n' => self.newline(), @@ -121,21 +150,4 @@ pub const Console = struct { } }; -pub const SerialConsole = struct { - /// Serial-only output: goes to the machine-readable log but *not* the framebuffer, -/// so debug and test detail stays out of the on-screen console. These are free -/// functions, not `Console` methods, because serial has no dependency on the -/// framebuffer — they work even before `con` is initialised. -pub fn debugWrite(bytes: []const u8) void { - arch.serialWrite(bytes); - } - - /// Formatted serial-only output, e.g. `debugPrint("x={d}\n", .{x})`. Truncates - /// past 256 bytes, like `Console.print`. -pub fn debugPrint(comptime fmt: []const u8, args: anytype) void { - var buf: [256]u8 = undefined; - debugWrite(std.fmt.bufPrint(&buf, fmt, args) catch return); - } - -}; diff --git a/src/kernel/log.zig b/src/kernel/log.zig new file mode 100644 index 0000000..bb2462a --- /dev/null +++ b/src/kernel/log.zig @@ -0,0 +1,83 @@ +//! The kernel's multi-sink **diagnostic** log — the machine-readable stream of +//! what the kernel is doing, separate from any user-facing display. +//! +//! Output is a *diagnostic convenience, never a correctness dependency* — the +//! kernel must boot and run correctly with zero output channels. So logging fans +//! out to a set of registered **sinks**, each best-effort and self-guarding: the +//! serial UART, the 0xE9 debug console, and — later — a file on a ramdisk/USB/SSD. +//! A message reaches whatever channels exist; if none do, the kernel runs on, +//! silent but correct. +//! +//! The **framebuffer is deliberately not a sink here.** It's a separate output +//! surface (a bootstrap text console today, a graphics device driver later), so +//! the log never assumes the machine is text-based. `main.zig` mirrors a few +//! user-facing status lines and panics to it explicitly; the verbose log does not. +//! +//! No allocation: the sink table is fixed, so the log works before the heap is up +//! and inside a panic. Two channels don't go through the sink list because they +//! must survive even a total-output failure: `checkpoint` (a one-byte POST code) +//! and `recordPanic` (a breadcrumb in a fixed record). + +const std = @import("std"); +const arch = @import("arch"); + +pub const SinkFn = *const fn ([]const u8) void; + +const max_sinks = 8; +var sinks: [max_sinks]SinkFn = undefined; +var sink_count: usize = 0; + +/// Register an output sink. Every registered sink receives every message; sinks +/// must be self-guarding (safe to call when their device is absent). +pub fn addSink(sink: SinkFn) void { + if (sink_count < max_sinks) { + sinks[sink_count] = sink; + sink_count += 1; + } +} + +/// Fan `bytes` out to every registered sink. +pub fn write(bytes: []const u8) void { + for (sinks[0..sink_count]) |sink| sink(bytes); +} + +/// A formatted log line. Truncates past 256 bytes; the buffer is on the stack, so +/// this is safe to call from interrupt context and from a panic. +pub fn print(comptime fmt: []const u8, args: anytype) void { + var buf: [256]u8 = undefined; + write(std.fmt.bufPrint(&buf, fmt, args) catch return); +} + +/// Emit a one-byte checkpoint/POST code (I/O port 0x80) — the always-available +/// progress channel for when there is no text output at all. Independent of the +/// sink list, so it works even before any sink is registered. +pub fn checkpoint(code: u8) void { + arch.postCode(code); +} + +// --- persistent panic breadcrumb ------------------------------------------- +// +// A fixed record in the kernel image that a panic fills in, so a post-mortem — an +// attached debugger, a RAM dump, or (later) a file/pstore reader — can recover +// what killed the kernel even when there was no live console. `magic` is written +// *last*, so a reader only trusts a fully-written record. + +pub const panic_magic: u64 = 0xD1ED_B00B_5EED_F00D; + +pub const PanicRecord = extern struct { + magic: u64 = 0, + len: u32 = 0, + _pad: u32 = 0, + msg: [512]u8 = undefined, +}; + +/// Findable by symbol (`log.panic_record`) for a debugger or RAM dump. +pub var panic_record: PanicRecord = .{}; + +/// Stamp the panic message into the breadcrumb record. +pub fn recordPanic(msg: []const u8) void { + const n: u32 = @intCast(@min(msg.len, panic_record.msg.len)); + @memcpy(panic_record.msg[0..n], msg[0..n]); + panic_record.len = n; + panic_record.magic = panic_magic; // set last: a reader sees a complete record +} diff --git a/src/kernel/main.zig b/src/kernel/main.zig index 8a5a0c4..9c39835 100644 --- a/src/kernel/main.zig +++ b/src/kernel/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const danos = @import("danos"); const arch = @import("arch"); const console = @import("console.zig"); +const log = @import("log.zig"); const pmm = @import("pmm.zig"); const heap = @import("heap.zig"); const scheduler = @import("scheduler.zig"); @@ -17,10 +18,17 @@ const BootInfo = danos.BootInfo; /// register the other expects. `danos.kernel_abi` re-exports it to the loader. pub const kernel_abi = danos.kernel_abi; -/// The system console, valid once `kmain` has initialised it. Global so the -/// panic handler can reach it too. -var con: console.Console = undefined; -var con_ready = false; +// POST/checkpoint codes emitted to I/O port 0x80 at boot milestones — the +// last-resort progress signal on a machine with no text output at all. +const cp_entry = 0x10; +const cp_paging = 0x20; +const cp_heap = 0x30; +const cp_discovery = 0x40; +const cp_scheduler = 0x50; +const cp_timer = 0x60; +const cp_running = 0x70; +const cp_exception = 0xE0; +const cp_panic = 0xEE; /// Kernel entry point. The bootloader jumps here after `ExitBootServices` with a /// pointer to the handoff data. There is no runtime, no stack unwinding, and no @@ -30,28 +38,40 @@ export fn _start(boot_info: *const BootInfo) callconv(kernel_abi) noreturn { } fn kmain(boot_info: *const BootInfo) noreturn { - arch.serialInit(); // machine-readable log; console mirrors to it + // The **log** is the machine-readable diagnostic stream: it fans out to every + // *diagnostic* channel that exists (serial, the 0xE9 debug console, and later a + // file on a ramdisk/USB/SSD), so a message survives as long as any is present. + // A headless, serial-less machine still boots correctly — it just goes quiet, + // with port-0x80 checkpoints as the only progress signal. + arch.serialInit(); + log.addSink(arch.serialWrite); + if (arch.debugconPresent()) log.addSink(arch.debugconWrite); + // The **framebuffer** is deliberately *not* a log sink. It's a separate output + // surface — a bootstrap text console today, a graphics device driver later — so + // we never assume the OS is text-based. Only a few user-facing status lines + // (via `status`) and panics are mirrored to it; the verbose log stays out. const fb = boot_info.framebuffer; - const serial0 = console.SerialConsole; - con = console.Console.init(fb); - con.clear(); - con_ready = true; + console.init(fb); + + log.checkpoint(cp_entry); // Catch CPU exceptions before doing anything that might fault: install our // reporter, then bring up the GDT + IDT. arch.setFaultHandler(onException); arch.init(); - con.write("danos: initalizing kernel..."); - - serial0.debugWrite("danos: framebuffer console online\n"); - serial0.debugWrite("danos: cpu tables online (GDT, IDT, TSS)\n"); - serial0.debugPrint(" resolution : {d}x{d}\n", .{ fb.width, fb.height }); - serial0.debugPrint(" pitch : {d} bytes\n", .{fb.pitch}); - serial0.debugPrint(" format : {s}\n", .{@tagName(fb.format)}); - serial0.debugPrint(" framebuffer: 0x{x:0>16}\n", .{fb.base}); - serial0.debugPrint (" footprint : {d} MiB\n", .{(fb.pitch * fb.height) / (1024 * 1024)}); + status("danos: initialising kernel...\n"); + log.write(if (console.present()) + "danos: framebuffer console online (bootstrap; graphics driver later)\n" + else + "danos: no framebuffer (headless) -> logging to serial/debugcon only\n"); + log.write("danos: cpu tables online (GDT, IDT, TSS)\n"); + log.print(" resolution : {d}x{d}\n", .{ fb.width, fb.height }); + log.print(" pitch : {d} bytes\n", .{fb.pitch}); + log.print(" format : {s}\n", .{@tagName(fb.format)}); + log.print(" framebuffer: 0x{x:0>16}\n", .{fb.base}); + log.print (" footprint : {d} MiB\n", .{(fb.pitch * fb.height) / (1024 * 1024)}); // Summarise the physical memory the loader handed us. The array is danos's // own MemoryRegion, so this is a plain slice — no firmware layout in sight. @@ -69,39 +89,41 @@ fn kmain(boot_info: *const BootInfo) noreturn { const total_bytes = total_pages * danos.page_size; const gib = 1 << 30; - serial0.debugWrite("\ndanos: physical memory\n"); - serial0.debugPrint(" total RAM : {d}.{d:0>2} GiB ({d} MiB) - RAM the firmware reported\n", .{ total_bytes / gib, (total_bytes % gib) * 100 / gib, mib(total_pages) }); - serial0.debugPrint(" usable : {d} MiB - free RAM (incl. reclaimed boot-services memory)\n", .{mib(usable_pages)}); - serial0.debugPrint(" reserved : {d} MiB - kernel image, boot stack, ACPI, runtime services\n", .{mib(reserved_pages)}); - serial0.debugPrint(" regions : {d} - entries in the firmware memory map\n", .{regions.len}); + log.write("\ndanos: physical memory\n"); + log.print(" total RAM : {d}.{d:0>2} GiB ({d} MiB) - RAM the firmware reported\n", .{ total_bytes / gib, (total_bytes % gib) * 100 / gib, mib(total_pages) }); + log.print(" usable : {d} MiB - free RAM (incl. reclaimed boot-services memory)\n", .{mib(usable_pages)}); + log.print(" reserved : {d} MiB - kernel image, boot stack, ACPI, runtime services\n", .{mib(reserved_pages)}); + log.print(" regions : {d} - entries in the firmware memory map\n", .{regions.len}); // Bring up the physical frame allocator over that map, and prove it works: // allocate three frames, then hand them back. pmm.init(boot_info.memory_map); const s1 = pmm.stats(); - serial0.debugPrint("\ndanos: frame allocator online\n", .{}); - serial0.debugPrint(" free frames: {d} ({d} MiB)\n", .{ s1.free_frames, mib(s1.free_frames) }); + log.print("\ndanos: frame allocator online\n", .{}); + log.print(" free frames: {d} ({d} MiB)\n", .{ s1.free_frames, mib(s1.free_frames) }); const f0 = pmm.alloc(); const f1 = pmm.alloc(); const f2 = pmm.alloc(); - serial0.debugPrint(" alloc x3 : 0x{x} 0x{x} 0x{x}\n", .{ f0 orelse 0, f1 orelse 0, f2 orelse 0 }); + log.print(" alloc x3 : 0x{x} 0x{x} 0x{x}\n", .{ f0 orelse 0, f1 orelse 0, f2 orelse 0 }); if (f0) |p| pmm.free(p); if (f1) |p| pmm.free(p); if (f2) |p| pmm.free(p); - serial0.debugPrint(" after free : {d} frames free\n", .{pmm.stats().free_frames}); + log.print(" after free : {d} frames free\n", .{pmm.stats().free_frames}); // Switch off the firmware's page tables onto our own (with real permissions). arch.enablePaging(pmm.alloc, boot_info); - serial0.debugPrint("\ndanos: paging enabled\n", .{}); - serial0.debugPrint(" page tables: CR3 = 0x{x:0>16}\n", .{arch.readCr3()}); - serial0.debugPrint(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_info.kernel_segment_count}); + log.checkpoint(cp_paging); + log.print("\ndanos: paging enabled\n", .{}); + log.print(" page tables: CR3 = 0x{x:0>16}\n", .{arch.readCr3()}); + log.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_info.kernel_segment_count}); // Bring up the kernel heap (dynamic allocation), built on the VMM. heap.init(); - serial0.debugWrite("\ndanos: kernel heap online\n"); + log.checkpoint(cp_heap); + log.write("\ndanos: kernel heap online\n"); // Measure the amount of resources the kernel is actually using const s2 = pmm.stats(); - serial0.debugPrint(" Kernel footprint: {d} KiB\n", .{kib(s1.free_frames - s2.free_frames)}); + log.print(" Kernel footprint: {d} KiB\n", .{kib(s1.free_frames - s2.free_frames)}); // Enumerate hardware from the firmware tables (ACPI here) into a generic // device tree, then list it. Discovery walks ACPI memory directly (identity- @@ -114,23 +136,23 @@ fn kmain(boot_info: *const BootInfo) noreturn { }; if (platform.discover(boot_info, heap.allocator(), hal)) |devtree| { var dt = devtree; - serial0.debugWrite("\ndanos: device discovery online\n"); - dt.dump(console.SerialConsole.debugWrite); + log.write("\ndanos: device discovery online\n"); + dt.dump(log.write); // Power register map extracted from the FADT + AML, for confidence it parsed. const pw = platform.powerInfo(); - serial0.debugWrite("danos: power\n"); - serial0.debugPrint(" pm1a_cnt : {s} 0x{x} (width {d})\n", .{ if (pw.pm1a_cnt.mmio) "mmio" else "io", pw.pm1a_cnt.address, pw.pm1a_cnt.width }); + log.write("danos: power\n"); + log.print(" pm1a_cnt : {s} 0x{x} (width {d})\n", .{ if (pw.pm1a_cnt.mmio) "mmio" else "io", pw.pm1a_cnt.address, pw.pm1a_cnt.width }); if (pw.s5) |s| { - serial0.debugPrint(" S5 slp_typ : a={d} b={d}\n", .{ s.slp_typ_a, s.slp_typ_b }); + log.print(" S5 slp_typ : a={d} b={d}\n", .{ s.slp_typ_a, s.slp_typ_b }); } else { - serial0.debugWrite(" S5 slp_typ : (not found)\n"); + log.write(" S5 slp_typ : (not found)\n"); } - serial0.debugPrint(" reset : supported={} {s} 0x{x} val 0x{x}\n", .{ pw.reset_supported, if (pw.reset.mmio) "mmio" else "io", pw.reset.address, pw.reset_value }); + log.print(" reset : supported={} {s} 0x{x} val 0x{x}\n", .{ pw.reset_supported, if (pw.reset.mmio) "mmio" else "io", pw.reset.address, pw.reset_value }); // AML namespace parse integrity: consumed should equal total. const am = platform.amlStats(); - serial0.debugPrint(" aml : {d} namespace nodes, parsed {d}/{d} bytes\n", .{ am.nodes, am.consumed, am.total }); + log.print(" aml : {d} namespace nodes, parsed {d}/{d} bytes\n", .{ am.nodes, am.consumed, am.total }); // Feed the arch layer the discovered addresses/facts so it makes no legacy // assumptions — the point of all this on UEFI Class 3 firmware. MMIO bases @@ -167,30 +189,33 @@ fn kmain(boot_info: *const BootInfo) noreturn { }); if (pinfo.spcr_uart) |u| arch.serialReconfigure(u.mmio, u.address); - serial0.debugWrite("danos: platform\n"); - serial0.debugPrint(" 8259 PIC : {s}\n", .{if (pinfo.pic_present) "present" else "absent"}); - serial0.debugPrint(" lapic base : 0x{x}\n", .{pinfo.lapic_base}); - serial0.debugPrint(" hpet base : 0x{x}\n", .{hpet_base}); - serial0.debugPrint(" pm timer : {s} 0x{x} ({s})\n", .{ if (pinfo.pm_timer.mmio) "mmio" else "io", pinfo.pm_timer.address, if (pinfo.pm_timer_32bit) "32-bit" else "24-bit" }); + log.write("danos: platform\n"); + log.print(" 8259 PIC : {s}\n", .{if (pinfo.pic_present) "present" else "absent"}); + log.print(" lapic base : 0x{x}\n", .{pinfo.lapic_base}); + log.print(" hpet base : 0x{x}\n", .{hpet_base}); + log.print(" pm timer : {s} 0x{x} ({s})\n", .{ if (pinfo.pm_timer.mmio) "mmio" else "io", pinfo.pm_timer.address, if (pinfo.pm_timer_32bit) "32-bit" else "24-bit" }); if (pinfo.spcr_uart) |u| { - serial0.debugPrint(" console UART: {s} 0x{x} (SPCR type {d})\n", .{ if (u.mmio) "mmio" else "io", u.address, pinfo.spcr_kind }); + log.print(" console UART: {s} 0x{x} (SPCR type {d})\n", .{ if (u.mmio) "mmio" else "io", u.address, pinfo.spcr_kind }); } else { - serial0.debugWrite(" console UART: none in SPCR -> legacy COM1\n"); + log.write(" console UART: none in SPCR -> legacy COM1\n"); } - serial0.debugPrint(" ioapic : base 0x{x}, {d} inputs (masked); entry0 low 0x{x}\n", .{ ioapic_base, arch.ioapicEntryCount(), arch.ioapicEntryLow(0) }); + log.print(" ioapic : base 0x{x}, {d} inputs (masked); entry0 low 0x{x}\n", .{ ioapic_base, arch.ioapicEntryCount(), arch.ioapicEntryLow(0) }); } else |err| { - serial0.debugPrint("\ndanos: device discovery failed: {s}\n", .{@errorName(err)}); + log.print("\ndanos: device discovery failed: {s}\n", .{@errorName(err)}); } + log.checkpoint(cp_discovery); // Register the current context as the first task before enabling preemption. scheduler.init(4); - serial0.debugWrite("\ndanos: scheduler online\n"); + log.checkpoint(cp_scheduler); + log.write("\ndanos: scheduler online\n"); // Start the timer and unmask interrupts — the kernel now has a heartbeat, and // the timer preempts among tasks. arch.startTimer(); arch.enableInterrupts(); - serial0.debugPrint("danos: timer online ({d} Hz tick; LAPIC {d} MHz, TSC {d} MHz; calibrated via {s})\n", .{ arch.timer_hz, arch.lapicHz() / 1_000_000, arch.tscHz() / 1_000_000, arch.timerCalibrationSource() }); + log.checkpoint(cp_timer); + log.print("danos: timer online ({d} Hz tick; LAPIC {d} MHz, TSC {d} MHz; calibrated via {s})\n", .{ arch.timer_hz, arch.lapicHz() / 1_000_000, arch.tscHz() / 1_000_000, arch.timerCalibrationSource() }); // In a test build (`zig build -Dtest-case=`), run that case and stop. // Normal builds fall through to the idle halt. @@ -199,15 +224,29 @@ fn kmain(boot_info: *const BootInfo) noreturn { arch.halt(); } - con.write("kernel initialised.\n"); + log.checkpoint(cp_running); + status("kernel initialised.\n"); // TODO: init process - con.write("\nnothing left to do; halting CPU.\n"); + status("\nnothing left to do; halting CPU.\n"); arch.halt(); } +/// A user-facing status line: to the diagnostic `log` *and* the on-screen console +/// (if a framebuffer is present). The verbose log uses `log.*` directly and never +/// touches the framebuffer. +fn status(msg: []const u8) void { + log.write(msg); + console.write(msg); +} + +fn statusPrint(comptime fmt: []const u8, args: anytype) void { + var buf: [256]u8 = undefined; + status(std.fmt.bufPrint(&buf, fmt, args) catch return); +} + /// Frames (4 KiB pages) to whole MiB. fn mib(pages: u64) u64 { return pages * danos.page_size / (1024 * 1024); @@ -217,32 +256,35 @@ fn kib(frames: u64) u64 { return frames * danos.page_size / (1024); } -/// Report a CPU exception in red and halt. There's no fault recovery yet, so any -/// exception is terminal — but now it debugPrints what and where instead of silently -/// resetting the machine. +/// Report a CPU exception and halt. There's no fault recovery yet, so any +/// exception is terminal — but it reports what and where (to every output sink, +/// plus a POST code and a persistent breadcrumb) instead of silently resetting. fn onException(state: *const arch.CpuState) noreturn { - if (con_ready) { - con.fg = 0x00ff_5555; - con.print("\nCPU EXCEPTION: {s} (vector {d})\n", .{ arch.vectorName(state.vector), state.vector }); - con.print(" error code : 0x{x}\n", .{state.error_code}); - con.print(" RIP : 0x{x:0>16}\n", .{state.rip}); - con.print(" RSP : 0x{x:0>16}\n", .{state.rsp}); - if (state.vector == 14) con.print(" CR2 (addr) : 0x{x:0>16}\n", .{arch.readCr2()}); - } + log.checkpoint(cp_exception); + // A fault is user-facing enough to paint on screen too (via statusPrint), on + // top of the diagnostic log. + statusPrint("\nCPU EXCEPTION: {s} (vector {d})\n", .{ arch.vectorName(state.vector), state.vector }); + statusPrint(" error code : 0x{x}\n", .{state.error_code}); + statusPrint(" RIP : 0x{x:0>16}\n", .{state.rip}); + statusPrint(" RSP : 0x{x:0>16}\n", .{state.rsp}); + if (state.vector == 14) statusPrint(" CR2 (addr) : 0x{x:0>16}\n", .{arch.readCr2()}); + + var buf: [128]u8 = undefined; + log.recordPanic(std.fmt.bufPrint(&buf, "CPU exception {s} (vector {d}) at RIP 0x{x}", .{ arch.vectorName(state.vector), state.vector, state.rip }) catch "cpu exception"); arch.halt(); } -/// Freestanding has no OS to receive a panic. debugPrint it to the console (if it is -/// up yet) in red, then halt. +/// Freestanding has no OS to receive a panic. Emit it to every output sink, drop a +/// POST code + a persistent breadcrumb (so a post-mortem can recover it even with +/// no live console), then halt. Assumes no console — the sinks self-guard. pub const panic = std.debug.FullPanic(struct { fn panic(msg: []const u8, first_trace_addr: ?usize) noreturn { _ = first_trace_addr; - if (con_ready) { - con.fg = 0x00ff_5555; - con.write("\nKERNEL PANIC: "); - con.write(msg); - con.write("\n"); - } + log.checkpoint(cp_panic); + log.recordPanic(msg); + status("\nKERNEL PANIC: "); + status(msg); + status("\n"); arch.halt(); } }.panic); diff --git a/src/root.zig b/src/root.zig index 3651225..25924e0 100644 --- a/src/root.zig +++ b/src/root.zig @@ -24,12 +24,21 @@ pub const PixelFormat = enum(u32) { /// A linear framebuffer: `width`x`height` pixels, each a 32-bit value, with /// `pitch` bytes between the start of one row and the next (which may be larger /// than `width * 4` due to hardware padding). +/// +/// A `base` of 0 means **no framebuffer** — the firmware exposed no Graphics +/// Output Protocol (a headless server, say). The kernel must treat on-screen +/// output as optional and never assume a framebuffer exists. pub const Framebuffer = extern struct { - base: usize, // the memory address where pixel data starts + base: usize, // the memory address where pixel data starts (0 = none) width: u32, // visible pixels per row (e.g. 1920) height: u32, // visible rows (e.g. 1080) pitch: u32, // bytes from the start of one row to the start of the next format: PixelFormat, + + /// Whether a usable framebuffer was handed over. + pub fn present(self: Framebuffer) bool { + return self.base != 0 and self.width != 0 and self.height != 0; + } }; /// Page size the memory map is measured in. 4 KiB on every architecture danos