add multi-sink diagnostic log; make framebuffer optional
This commit is contained in:
@@ -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
|
||||
|
||||
+44
-32
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+114
-72
@@ -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=<name>`), 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);
|
||||
|
||||
Reference in New Issue
Block a user