Repository navigation
Building and running
The only thing you install on your machine is Docker. There is no
host-side build: i686-elf-gcc, nasm, qemu-system-i386, grub-mkrescue
and xorriso all live inside the image k-ernel-toolchain, built from
docker/Dockerfile.
Running make on the host will fail, and that is intended — the Makefile
says so in its header.
./docker/run.shdocker/run.sh
does four things:
- checks the Docker daemon is up; on macOS, if it is not, it runs
open -a Dockerand waits up to 90 seconds for the socket; - builds (or reuses from cache) the image tagged
k-ernel-toolchain; - mounts the repository at
/kernelinside the container, which is also the image's working directory; - gives you an interactive
bash, with--rm: the container is thrown away when you exit.
Because the repository is a bind mount, edits made by your editor on the host
are visible to the next make immediately, and build output appears on the
host under build/. Nothing you care about lives inside the container — which
is why --rm is safe.
The image carries i686-elf-gcc 13.2.0 and GNU binutils 2.42, both built from
source into /opt/cross (on PATH), plus NASM, GNU Make, GDB, QEMU, and GRUB
with grub-pc-bin, grub-common, xorriso and mtools for the ISO.
The first line of the Dockerfile is FROM --platform=linux/amd64 debian:bookworm-slim, and docker/run.sh passes --platform linux/amd64 to
both docker build and docker run.
The reason is grub-pc-bin: it contains the x86 BIOS boot images that
grub-mkrescue embeds into a bootable ISO, and Debian does not publish it for
arm64. No grub-pc-bin, no bootable ISO.
On an ARM host (Apple Silicon included) Docker therefore runs the whole
container under emulation. The consequence to plan for is the first build:
it compiles binutils and GCC from source, emulated, which takes hours. It
happens exactly once — afterwards the layer cache makes docker build return
immediately, and run.sh is instant. Compiling the kernel itself is fast even
emulated; it is a handful of small translation units.
All of these run inside the container, from /kernel. The list is the
whole of
Makefile's
public surface — if a command is not here, it does not exist.
| Target | What it does |
|---|---|
make (all) |
Compiles and links build/kernel.bin, then runs grub-file --is-x86-multiboot on it as part of the link rule: a kernel without a valid Multiboot header fails the build instead of failing in GRUB. |
make check |
The same Multiboot check on its own, for when you have just touched boot.s or linker.ld. |
make iso |
Lays out build/isodir/boot/kernel.bin and build/isodir/boot/grub/grub.cfg, then builds build/k-ernel.iso with grub-mkrescue. |
make run |
qemu-system-i386 -cdrom build/k-ernel.iso -display none -serial stdio |
make run-vga |
qemu-system-i386 -cdrom build/k-ernel.iso -display curses -serial file:build/serial.log |
make debug |
make run plus -s -S: QEMU starts halted with a GDB stub on port 1234. |
make clean |
rm -rf build — the whole build tree, ISO included. |
Objects mirror the source tree under build/ (build/arch/x86/vga.o), and
header dependencies are tracked with -MMD -MP, so touching a header does
rebuild what includes it.
One detail worth knowing: the architecture-private include path (-Iarch/x86)
is granted only to objects under build/arch/x86/. A #include "io.h"
from kernel/ is not a style violation, it is a compile error. That seam is
the reason the tree is split the way it is.
grub.cfg sets timeout=0 with a single menuentry "K-ernel", so there is no
menu to click through — GRUB boots the kernel straight away. On the serial line
you get the banner with the version, the self-test run, and the Multiboot magic
check. Then the kernel parks the CPU in a halt loop: it never exits, by design.
Ctrl-C. The Ctrl-A X sequence from most tutorials is QEMU's multiplexer
escape and does not apply to these targets; the README explains why.
-
make run— straight into your terminal, mixed with nothing else. This is the normal way to read a boot. -
make run-vga— the terminal is taken by the curses rendering of the real VGA screen, so the serial log is redirected tobuild/serial.log. Watch it from a second shell withtail -f build/serial.log.-display cursesneeds a real interactive terminal. -
make debug— same asmake run, with QEMU halted until GDB connects.
run.sh does not name the container, so look the id up first:
docker ps # on the host
docker exec -it <id> bash # same /kernel, same build treeThat is how you attach GDB while make debug is waiting (the README has the
exact gdb invocation), or tail build/serial.log during make run-vga.
Handy when you only want to know whether something compiles, or in a script:
docker run --rm --platform linux/amd64 -v "$PWD":/kernel k-ernel-toolchain \
sh -c 'make clean && make iso'And since the kernel never terminates, capture a boot with a timeout instead of waiting for it:
docker run --rm --platform linux/amd64 -v "$PWD":/kernel k-ernel-toolchain \
sh -c 'timeout 20 qemu-system-i386 -cdrom build/k-ernel.iso \
-display none -serial stdio'Both rely on the image's working directory already being /kernel.