Skip to content

Building and running

Matteo Checcacci edited this page Oct 14, 2026 · 1 revision

Building and running

The toolchain lives in the container, nowhere else

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.sh

docker/run.sh does four things:

  1. checks the Docker daemon is up; on macOS, if it is not, it runs open -a Docker and waits up to 90 seconds for the socket;
  2. builds (or reuses from cache) the image tagged k-ernel-toolchain;
  3. mounts the repository at /kernel inside the container, which is also the image's working directory;
  4. 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.

Why the image is pinned to linux/amd64

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.

Make targets

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.

Booting: what you will see

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.

Quitting QEMU

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.

Where the serial output goes

  • 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 to build/serial.log. Watch it from a second shell with tail -f build/serial.log. -display curses needs a real interactive terminal.
  • make debug — same as make run, with QEMU halted until GDB connects.

A second shell in the running container

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 tree

That 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.

Building without a shell

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.