Repository navigation
Troubleshooting
Ordered by how likely you are to hit it, not by how interesting it is.
The Docker CLI is useless without a running daemon; on macOS that means Docker
Desktop must be open, with the whale in the menu bar. docker/run.sh tries to
start it for you (open -a Docker) and waits up to 90 seconds, then gives up
with a message. On Linux it refuses outright: start the daemon and retry.
Corollary that costs real time: quitting Docker Desktop kills whatever is running inside it. If you close it during the first image build, the GCC compile stops where it is. Layer caching means a restart resumes from the last completed step rather than from zero, but the step in progress is lost.
Do not. In a freestanding kernel a warning is usually a bug that still links
and still boots. The build compiles with -Wall -Wextra and nothing else is
assumed: GCC will happily produce a working ISO out of code that returns a
value from a void function, or indents a statement outside the if that was
supposed to guard it. "It compiled and it booted" is not evidence.
What makes this nasty is that the build is incremental: a warning is printed by
the compiler only when that file is actually recompiled. Run make twice and
the second run is silent — the warning has not been fixed, it has been cached
away. So:
make clean && makeis the only run whose output you can trust. Do it before you conclude the tree is clean, and before you report that it is not.
make decides what to rebuild by comparing modification times, and here those
timestamps are written by your editor on the host and read by make
through a bind mount inside an emulated container. When the two disagree — a
file saved with an older mtime, a clock that drifted, an editor that writes
atomically through a rename — make concludes there is nothing to do and keeps
the objects it already has.
The failure mode is not an error message. It is QEMU booting the previous
build/k-ernel.iso while you watch for a change you just made and wonder why
it had no effect, or why a bug you fixed is still there.
Check before you debug:
ls -l build/kernel.bin build/k-ernel.isoIf those are older than the source you edited, you are testing the past. Force
the point with touch on the file, or settle it with make clean && make iso.
Any time a result surprises you, a clean rebuild is the first thing to rule
out, not the last.
Reading the hardware state back is the right instinct — it is how the GDT and
IDT self tests work. But the monitor answers about the machine as it is right
now, and if you ask too early "right now" is the reset state, or the firmware,
or GRUB: a GDT limit of 0xffff, cs=0xf000, no TSS. Nothing is wrong; your
kernel simply has not run yet.
No make target attaches a monitor, so when you want one, run QEMU by hand
inside the container and keep the serial port out of the way:
qemu-system-i386 -cdrom build/k-ernel.iso -display none \
-monitor stdio -serial file:build/serial.logThen wait: query only after build/serial.log shows the self-test verdict,
which means the kernel has reached its halt loop. info registers at that
point reports what the kernel really loaded. Asking at the (qemu) prompt the
instant it appears reports what the BIOS left behind.
A language server on the host (clangd and friends) analyses this code with the
host's own compiler and target — on an Apple Silicon machine, an arm64 clang
with a hosted standard library. This code is i686, freestanding, built with a
cross compiler that has no target OS and no libc, and some of it is i686 inline
assembly and __attribute__((packed)) structs.
So the editor invents errors: unknown register names, unresolved <stdint.h>,
complaints about freestanding constructs, wrong sizeof reasoning. None of
them are real. The container build is the truth; the editor index is a
convenience. Note that compile_commands.json is git-ignored precisely because
it describes one machine's idea of the build, not the build.
With make run-vga that is the design: the terminal is showing the VGA screen,
and the serial log is in build/serial.log (tail -f it from a second shell).
With make run the serial output is the terminal, so silence means the kernel
produced none — see below.
An unhandled CPU exception is supposed to be loud: the kernel prints
--- EXCEPTION <n>: <mnemonic> ---, dumps every register including eip and
the error code, and halts. A divide by zero stops with an explanation; it does
not reset.
A silent reset therefore means the fault happened somewhere that path could not cover:
- before output was initialised at all (very early in
boot.s, or a bad Multiboot handover); - a double fault, which currently has no handler of its own — a stack overflow reboots without a message (issue #2);
- an NMI, which has a gate but no handler (issue #1).
Both are known and tracked. To find out where, make debug and attach GDB (the
README has the invocation): a breakpoint on kernel_main tells you
immediately whether you are even getting that far.