117 lines
4.9 KiB
Zig
117 lines
4.9 KiB
Zig
//! 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 architecture = @import("architecture");
|
|
|
|
pub const SinkFn = *const fn ([]const u8) void;
|
|
|
|
const maximum_sinks = 8;
|
|
var sinks: [maximum_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 < maximum_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);
|
|
}
|
|
|
|
// --- the RAM sink: a retained copy of the whole diagnostic stream ------------
|
|
//
|
|
// A fixed in-image buffer that accumulates every logged byte, so a user program
|
|
// (`log-flush`, and init at shutdown) can read it back through `klog_read` and
|
|
// persist it to a file — the boot log survives on a headless/real machine that
|
|
// has no host capturing serial. It is a *sink like any other*: register it with
|
|
// `addSink(ramSink)` at boot. No allocation (works pre-heap and in a panic).
|
|
//
|
|
// It fills linearly and stops when full: the earliest output — the most valuable
|
|
// for diagnosing a boot — is kept, and the tail is still on the live serial sink.
|
|
// 256 KiB comfortably holds a full boot plus a long run (a boot is ~15 KiB).
|
|
|
|
const ram_capacity = 256 * 1024;
|
|
var ram_buffer: [ram_capacity]u8 = undefined;
|
|
var ram_len: usize = 0;
|
|
|
|
/// The RAM sink. Best-effort and self-guarding like every sink: appends what fits
|
|
/// and silently drops the rest once full. (Concurrency matches the other sinks —
|
|
/// the dominant writer, debug_write, already holds the kernel lock; a rare torn
|
|
/// append on a kernel-internal line is an accepted diagnostic imperfection.)
|
|
pub fn ramSink(bytes: []const u8) void {
|
|
const n = @min(ram_buffer.len - ram_len, bytes.len);
|
|
if (n != 0) {
|
|
@memcpy(ram_buffer[ram_len..][0..n], bytes[0..n]);
|
|
ram_len += n;
|
|
}
|
|
}
|
|
|
|
/// The accumulated log so far — what `klog_read` copies out.
|
|
pub fn ramSnapshot() []const u8 {
|
|
return ram_buffer[0..ram_len];
|
|
}
|
|
|
|
/// 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 buffer: [256]u8 = undefined;
|
|
write(std.fmt.bufPrint(&buffer, 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 {
|
|
architecture.checkpoint(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,
|
|
message: [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(message: []const u8) void {
|
|
const n: u32 = @intCast(@min(message.len, panic_record.message.len));
|
|
@memcpy(panic_record.message[0..n], message[0..n]);
|
|
panic_record.len = n;
|
|
panic_record.magic = panic_magic; // set last: a reader sees a complete record
|
|
}
|