← Back to writing

AstraeaOS — Back to Pure Assembly

After IsmenaOS's scope failure, a return to fundamentals. 17 files of assembly. The entire OS fits on one screen. Sometimes the most educational project is the smallest one.

After IsmenaOS’s ambition — C kernel, Swift runtime, custom libc — I went the other direction. AstraeaOS is 17 files of pure assembly. No C, no libc, no cross-compiler, no toolchain repository. Just NASM and the CPU.

17 Files

The entire OS:

astraeaos/
  boot/
    boot.asm           — Stage 1 bootloader (512 bytes, fits in MBR)
    stage2.asm         — Stage 2 loader (reads kernel from disk)
  kernel/
    kernel.asm         — Kernel entry point
    gdt.asm            — Global Descriptor Table
    idt.asm            — Interrupt Descriptor Table
    isr.asm            — Interrupt Service Routines
    irq.asm            — Hardware interrupt handlers
    pic.asm            — Programmable Interrupt Controller setup
    keyboard.asm       — PS/2 keyboard driver
    screen.asm         — VGA text mode output
    memory.asm         — Basic memory management
    string.asm         — String utilities
    ports.asm          — I/O port access
  link.ld              — Linker script
  Makefile             — Build (nasm + ld)
  run.sh              — QEMU boot script
  README.md

KairosOS needed 500+ files for a kernel that did roughly the same things. The difference: C requires a cross-compiler, a libc, a startup runtime (crt0), header files, a build system that manages compilation units, and a linker script that accounts for C’s memory model. Assembly requires an assembler and a linker. That’s it.

The Boot Sequence

The assembly kernel made the boot sequence completely transparent. In C, the boot process is hidden behind your bootloader and startup code — by the time kernel_main is called, protected mode is set up, the GDT is loaded, and the stack exists. You don’t see how you got there.

In assembly, you write every instruction of the journey:

Stage 1 (512 bytes, loaded by BIOS):

  1. BIOS loads the first sector (512 bytes) to 0x7C00
  2. Set up a minimal stack
  3. Load Stage 2 from disk using BIOS interrupt int 0x13
  4. Jump to Stage 2

Stage 2 (loaded by Stage 1):

  1. Enable A20 line (access memory above 1MB)
  2. Load the GDT — defines memory segments for protected mode
  3. Switch to protected mode: set PE bit in CR0, far jump to flush the pipeline
  4. Load the kernel from disk
  5. Jump to kernel entry point

Kernel entry:

  1. Set up the IDT — interrupt handlers for exceptions, hardware interrupts, syscalls
  2. Initialize the PIC — configure hardware interrupt routing
  3. Enable keyboard IRQ
  4. Print a welcome message via direct VGA memory writes
  5. Enter the main loop — handle interrupts, process keyboard input

Every step is an instruction I wrote. Nothing is implicit. When the CPU switches to protected mode, I can point to the exact mov cr0, eax and jmp 0x08:protected_mode_start that does it. When a key is pressed, I can trace the signal from the PIC through the IRQ handler to the keyboard driver to the character on screen.

What 17 Files Teaches That 500 Didn’t

The minimal viable kernel is smaller than you think. You need: a bootloader that switches to protected mode, a GDT, an IDT with at least exception handlers, a way to write to the screen, and an idle loop. Everything else — memory management, process scheduling, filesystem, networking — is optional. KairosOS’s 500 files included a build system, a libc, userspace stubs, and documentation infrastructure. The kernel itself was maybe 30 files. AstraeaOS’s 17 files proved that the minimum is even smaller.

Assembly is honest. There’s no compiler optimizing your code, no linker resolving symbols, no runtime initializing state. What you write is what runs. If the GDT is wrong, the CPU faults immediately. If the IDT is wrong, the first interrupt crashes the system. There’s no abstraction to blame — every bug is your bug.

C’s overhead is real but justified. After writing a keyboard driver in assembly — tracking scan codes, handling shift states, managing a key buffer — I understood why C exists. The assembly version is maybe 200 lines for a basic keyboard handler. The C version would be 50 lines and more readable. The assembly taught me what those 50 lines of C actually compile to. That’s the value: not “assembly is better” but “now I know what C does.”

The Comparison

What KairosOS needed 500+ files for, AstraeaOS achieves in 17. But “achieves” is generous — AstraeaOS can print text and handle keyboard input. KairosOS had a libc, a (basic) syscall interface, and the architecture for userspace programs. The 17-file kernel is complete in the sense that it boots and runs, but it’s a foundation, not a system.

The lesson isn’t that assembly is sufficient. It’s that stripping away every layer of abstraction reveals what the hardware actually needs from you — and it’s less than you’d think. The GDT needs to exist. The IDT needs to handle faults. The screen needs bytes written to 0xB8000. The keyboard needs IRQ1 handled. Everything else is choice.


This is Part 6 of 8 in the OS Kernel Museum series.