105 lines
5.1 KiB
Markdown
105 lines
5.1 KiB
Markdown
# 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
|
|
|
|
`system/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 `system/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 `system/boot-handoff.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.
|