After the assembly experiments proved I could write bytes that the CPU understood, the next question was obvious: can I write a real kernel? Not a bootloader that prints “Hello” — a kernel with a libc, a userspace, and maybe even a window manager.
KairosOS was the answer. Two versions, 501 and 567 files respectively. The most ambitious project I’d attempted. The most educational failure I’ve had.
The Plan
The architecture was clear in my head:
kairos_kernel/ — The kernel itself
kairos_libc/ — Custom C standard library
kairos_userspace/ — Userspace programs The endgame: boot the kernel, start a Lua runtime in userspace, launch Awesome WM as the desktop environment. A custom OS with a tiling window manager. On paper, a 6-month project. In practice, I never got past basic syscalls.
Building the Cross-Compiler
The first real work wasn’t kernel code — it was building the tools to compile kernel code. You can’t compile an OS with your system’s GCC because your system’s GCC targets your system’s OS. You need a cross-compiler that targets nothing — a freestanding environment with no libc, no POSIX headers, no assumptions about the platform.
# The GCC cross-compiler configure for KairosOS
../gcc-11.2.0/configure \
--target=x86_64-kairos-elf \
--prefix=/opt/cross \
--disable-nls \
--enable-languages=c \
--without-headers --target=x86_64-kairos-elf tells GCC to generate code for an x86_64 CPU running “kairos” — a target that doesn’t exist yet. --without-headers means don’t expect any OS headers. --enable-languages=c means just C, no C++ or Fortran.
Building this cross-compiler from source — downloading GCC and Binutils, configuring them, waiting through the compile — took longer than writing my first kernel code. It was the first time I understood that the toolchain isn’t a dependency you install. It’s a component you build, configure, and maintain. This lesson would define every kernel that followed.
The libc Problem
A C program that calls printf needs a C standard library. On Linux, that’s glibc or musl. On KairosOS, that’s… nothing. You have to write it.
But how much of it? The C standard library is enormous — stdio.h, stdlib.h, string.h, math.h, dozens of headers with hundreds of functions. You don’t need all of it. You need the subset your kernel and userspace actually use.
KairosOS’s libc started with three functions: memset, memcpy, and strlen. Not printf — that requires formatted output, which requires write, which requires a syscall interface, which requires the kernel to handle it. The dependency chain is deep, and each link has to be built before the next.
// kairos_libc/string/memset.c — the first libc function
void *memset(void *s, int c, size_t n) {
unsigned char *p = s;
while (n--) *p++ = (unsigned char)c;
return s;
} Three lines of actual logic. But getting to the point where this compiles, links against the kernel, and runs in a freestanding environment required understanding the difference between __STDC_HOSTED__ (has full libc) and freestanding (has almost nothing). The cross-compiler emits freestanding code by default — your libc fills in what’s missing, one function at a time.
What Worked
The kernel booted in QEMU. It switched from real mode to protected mode to long mode (64-bit), set up a GDT and IDT, initialized the console, and printed text to the screen. Basic syscalls worked — the kernel could handle write and exit from userspace programs linked against the custom libc.
The directory structure was clean. The build system (initially make, later bjam via Jamroot) could compile the kernel, the libc, and userspace programs separately and link them together. The separation of kairos_kernel, kairos_libc, and kairos_userspace into independent build targets was the right architecture.
What Didn’t Work
Lua integration was too ambitious. Embedding the Lua runtime requires a substantial libc — file I/O, memory allocation, string formatting. My libc had memset and strlen. The gap between “basic libc” and “enough libc for Lua” was months of work, and I didn’t have the patience or knowledge to grind through it.
The build system was fragile. I switched from Make to Bjam (Boost.Build’s predecessor) partway through, which is an unusual choice for a kernel project. Bjam is powerful but has a steep learning curve, and the documentation for cross-compilation use cases was thin. I spent more time fighting the build system than writing kernel code.
Debugging was brutal. When a kernel crashes, there’s no stack trace. There’s no printf (unless you’ve implemented it). There’s a triple fault and a QEMU reset. Debugging meant reading register dumps, inserting breakpoints in GDB connected to QEMU’s GDB stub, and reasoning about what the CPU was doing from a hex dump of memory. The assembly experiments helped — I could read the disassembly — but kernel debugging is a fundamentally different skill from application debugging.
KairosOS v2
Version 2 (567 files, os-kairos directory) was a refinement, not a rewrite. Better documentation, cleaner build system, an attempt at LLVM/Clang as an alternative to GCC. The code was incrementally better, but the fundamental limitation was the same: my libc wasn’t mature enough to support the userspace I wanted.
The Lesson
Scope your first kernel to what you can debug, not what you can imagine. I imagined a Lua desktop environment. I could debug memset. The gap between those two things was insurmountable in a single project, and I spent most of my time in the gap instead of building either end well.
The next kernel — Voleta — would apply this lesson by focusing entirely on the toolchain and libc, deferring the userspace question entirely.
This is Part 3 of 8 in the OS Kernel Museum series.