From 26d2f5259c35e02085b0d67be9d98ae165de6494 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:41:47 +0100 Subject: [PATCH] document the multi-sink logging model --- docs/README.md | 4 ++ docs/logging.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 docs/logging.md diff --git a/docs/README.md b/docs/README.md index f2cda0a..3cb78d1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -73,6 +73,10 @@ Cutting across all of these: - **[testing.md](testing.md) — testing.** How the kernel is tested by booting it in QEMU and asserting on its serial output — reproducibly, and structured so the same tests run across architectures. +- **[logging.md](logging.md) — logging.** The multi-sink diagnostic log (serial, + 0xE9 debugcon, file later) kept separate from the framebuffer display, plus the + robustness path: optional framebuffer, POST-code checkpoints, and a persistent + panic breadcrumb so the kernel survives — and can be diagnosed — with no output. ## How the pieces relate diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 0000000..98394af --- /dev/null +++ b/docs/logging.md @@ -0,0 +1,104 @@ +# Logging: the diagnostic log vs. the display + +danos separates two things that are easy to conflate: the **diagnostic log** — the +machine-readable stream of *what the kernel is doing* — and the **display**, the +framebuffer surface the OS draws on. They are different concerns with different +lifetimes, so they're different code paths. + +The guiding rule: **output is a diagnostic convenience, never a correctness +dependency.** The kernel must boot and run correctly with *zero* output channels — +no serial, no screen. Logging that can take the kernel down isn't robust; it's a +liability. This is the same [resilience](resilience.md) posture the rest of the +kernel follows. + +## The log is multi-sink + +`src/kernel/log.zig` is the diagnostic log. It fans a message out to a set of +registered **sinks**, each best-effort and self-guarding: + +```zig +log.addSink(arch.serialWrite); // the serial UART +if (arch.debugconPresent()) log.addSink(arch.debugconWrite); // 0xE9 debug console +// later: log.addSink(fs.logWrite); // a file on a ramdisk / USB / SSD +log.write("…"); log.print("x={d}\n", .{x}); +``` + +Properties that matter: + +- **No allocation.** The sink table is a fixed array, so the log works before the + heap is up and inside a panic. +- **Best-effort.** A sink whose device is absent is a no-op (e.g. writing to a + missing UART just goes nowhere — the TX-wait is bounded so it can't hang). A + message reaches whatever channels exist; if none do, the kernel runs on, silent. +- **Order-independent.** Every registered sink gets every message. Adding the file + logger later is one `addSink` call and **zero** changes to call sites. + +## The framebuffer is *not* a log sink + +The framebuffer is a general graphics surface, **not inherently a text terminal**. +Today `src/kernel/console.zig` paints a text grid on it as a *bootstrap* console, but +that's a stop-gap: once the driver machinery exists the framebuffer becomes a proper +**graphics device driver**, and the text crutch goes away. So the log must not assume +it — routing the verbose log through a pixel console would bake in "the OS is text". + +Instead the two paths are explicit: + +``` +verbose diagnostics ──► log ──► serial, debugcon, (file later) +user status / panics ──► status() ──► log (above) + framebuffer (if present) +``` + +A handful of user-facing lines (`kernel initialised`, a panic) go through +`main.zig`'s `status()` / `statusPrint()`, which write to the log **and** paint the +framebuffer when one is present. Everything else uses `log.*` and never touches the +screen. `console.write` is a no-op when the firmware gave us no framebuffer. + +## Optional framebuffer (headless machines) + +A framebuffer is not guaranteed — a headless server exposes no UEFI Graphics Output +Protocol. That used to be *fatal* (the loader failed the boot). Now the loader hands +over a "no framebuffer" descriptor (`base == 0`) rather than failing, and +`Framebuffer.present()` (in `src/root.zig`) gates every on-screen path. A headless, +serial-less machine boots and runs correctly — it just goes quiet. + +## Last-resort channels (no text output at all) + +Two signals bypass the sink list, because they must survive even a total +output-channel failure: + +- **`log.checkpoint(code)`** — a one-byte **POST code** to I/O port `0x80` (a POST + card or BMC shows it). `main.zig` emits one at each boot milestone (`cp_paging`, + `cp_heap`, …) and on a fault/panic, so "where did it hang?" is answerable with no + text output whatsoever. Writing `0x80` is universally safe. +- **`log.recordPanic(msg)`** — stamps the panic message into a fixed record + (`log.panic_record`, with a `magic` written last). A post-mortem — an attached + debugger, a RAM dump, or a future file/pstore reader — recovers *what killed it* + even though nothing was on screen. + +The panic and CPU-exception handlers fan out to every sink, emit a POST code, and +drop the breadcrumb — they never assume a console. + +## The 0xE9 debug console + +Port `0xE9` is the Bochs/QEMU debug console. It's detected safely: the port returns +`0xE9` when read if present, and `0xFF` on real hardware, so `debugconPresent()` +only enables the sink when it's really there. Under QEMU it's captured with +`-debugcon file:…`, giving CI a log channel independent of `-serial`. + +## The robustness spectrum + +The result handles every combination — framebuffer only, serial only, both, or +**neither**. With no channels at all the kernel still boots and runs; port-`0x80` +checkpoints track progress and the panic breadcrumb captures failures. *Runs blind +but correct* is the goal, not *always has output*. + +## Related + +- [framebuffer.md](framebuffer.md) — the display surface itself (pitch, format), the + thing that becomes a graphics device driver. +- [efi.md](efi.md) — where the loader captures (or, headless, doesn't capture) the + framebuffer before `ExitBootServices`. +- [device-interrupts.md](device-interrupts.md) — the serial UART bring-up the log's + primary sink rides on. +- [resilience.md](resilience.md) — why "never let a missing peripheral take the + kernel down" is a core design stance.