Skip to main content

Free 13-chapter course · C · Rust · AArch64 · QEMU

Build an Operating System from Scratch on ARM64

You are going to start with a powered-on AArch64 CPU and nothing else, and you are going to finish with a working kernel that boots into your own interactive shell. Everything is written in C and Rust and runs on QEMU, so there is no hardware to buy and nothing to pay for.

Chapters
13/13
Project time
25–40 hours
Explorers
1

What you'll build

qemu-system-aarch64 · PurgatoryOS
$ make run
qemu-system-aarch64 -M virt -cpu cortex-a53 -nographic -kernel kernel.elf
PurgatoryOS v1.0 booted at EL1
MMU: on · GIC: on · timer: 100Hz · tasks: 2
purgatory> ls /
bin/  docs/  README
purgatory> uptime
up 42 ticks, 2 tasks running
purgatory> ▊
The end state: your own kernel, booting your own shell.

Why ARM64, Not x86?

Most operating system tutorials, including the excellent OSDev wiki and Philipp Oppermann's Writing an OS in Rust, target x86. That choice comes with a price. Before you execute a single line of your own kernel, you have to fight through real mode, segmentation, the A20 gate, and a boot protocol that carries forty years of backwards compatibility on its shoulders. None of it teaches you anything about operating systems. It teaches you about the history of one processor family.

AArch64 removes almost all of that friction. The CPU starts in a well-defined exception level, memory-mapped I/O replaces port juggling, and the architecture's four privilege levels map cleanly onto the user and kernel boundary you actually came here to learn. You reach the interesting problems, like virtual memory, interrupts and scheduling, in hours rather than weeks.

It is also the pragmatic choice. ARM64 is the instruction set of every modern phone, every Apple Silicon Mac, and a rapidly growing share of cloud servers. The register conventions, page table formats and exception model you learn in this course are the ones running on the machine in your pocket right now.

x86 tutorial, week one
Real mode → protected mode → long mode transitions, GDT/IDT descriptor tables, legacy BIOS or UEFI boot shims.
This course, week one
Boot at EL1, print over the UART, take control of the linker, and go straight into the MMU and real memory protection.

How the Course Works

  • Everything runs on QEMU. The virt machine emulates a full AArch64 system, which means there is no development board to buy, nothing to flash, and a reboot cycle measured in seconds.
  • C first, then Rust. The core of the kernel is plain C that you can read line by line. In chapter 11 we carve out the heap allocator and replace it with a no_std Rust crate that talks to the rest of the kernel over the C FFI.
  • Every chapter ends bootable. You are never more than one chapter away from a kernel that runs, which does more for motivation than any amount of theory.
  • Failure is part of the syllabus. Each chapter documents what actually broke during development, from access flag faults to stale TLB entries, and walks through how to debug it.

Milestones

From reset vector to an interactive shell on QEMU's virt machine.

  1. 01 Toolchain
  2. 02 Boot
  3. 03 UART
  4. 04 Linker script
  5. 05 MMU
  6. 06 Interrupts
  7. 07 Heap
  8. 08 Processes
  9. 09 Syscalls
  10. 10 Rust FFI
  11. 11 Filesystem
  12. 12 Shell

The Full Curriculum

Every chapter is a self-contained milestone that ends with a kernel you can actually boot. Each one also has a “What Broke (And Why)” section where I document the bugs I hit while writing the code, because a silent QEMU hang will teach you more about the hardware than a clean happy path ever will.

  1. 01

    Why Build an OS from Scratch? The Case for Going All the Way Down

    Stop treating the OS as a black box. Discover why building an AArch64 kernel from scratch on ARM is the ultimate systems programming challenge for 2026. From silicon to shell, we go all the way down.

  2. 02

    AArch64 Toolchain Setup: Cross-Compiler, QEMU and Your First ARM Binary

    Step-by-step guide to installing an AArch64 cross-compiler (GCC) and QEMU on macOS and Linux. Write a bare-metal linker script and Makefile to boot your first ARM64 program.

  3. 03

    The ARM64 Boot Process: From Reset Vector to kernel_main

    Learn how to write an ARM64 bootloader stub in assembly. We cover AArch64 exception levels (EL2 to EL1), zeroing BSS, stack setup, and jumping to a C kernel_main on bare metal.

  4. 04

    PL011 UART Driver on QEMU: Your First Bare-Metal Kernel Output

    Learn how to write a bare-metal C driver for the PL011 UART on AArch64. Master Memory-Mapped I/O (MMIO), the volatile keyword in C, and building a kprint function for QEMU virt without a standard library.

  5. 05

    Linker Scripts & Kernel Memory Layout: Teaching the Linker What 'Correct' Looks Like

    Master ELF sections (.text, .rodata, .data, .bss) and fix alignment bugs in your AArch64 linker script. Learn to verify memory maps using readelf for bare-metal ARM development.

  6. 06

    AArch64 Virtual Memory & the MMU: Teaching the CPU to Lie

    Master the AArch64 MMU on bare metal ARM. A deep dive into page table structures, translation levels (L0-L3), and configuring SCTLR_EL1, TCR_EL1, and MAIR_EL1 for kernel memory protection.

  7. 07

    AArch64 Exceptions & Interrupts: Teaching Your Kernel to Listen

    Build an AArch64 exception vector table from scratch, set up the GICv2 Generic Interrupt Controller on QEMU, and write a working ARM Generic Timer interrupt handler. Your kernel finally responds to the hardware.

  8. 08

    Kernel Heap Allocator in C: Teaching the Kernel to Manage Its Own Memory

    Write a bump allocator and then a free-list allocator from scratch in C for your AArch64 bare-metal kernel. Learn block headers, first-fit search, coalescing, alignment, and why Rust's no_std offers a safer alternative. Visualize every allocation interactively.

  9. 09

    Context Switching & Process Scheduling: Teaching the Kernel to Multitask

    Build a round-robin task scheduler from scratch for your AArch64 bare-metal kernel. Implement the PCB, understand why only callee-saved registers need saving, write the context_switch assembly, and watch two tasks interleave on the UART, the moment your kernel becomes an actual operating system.

  10. 10

    How System Calls Work: Drawing the Line Between User and Kernel

    Drop your AArch64 tasks into EL0, trap into EL1 with the svc instruction, decode ESR_EL1 EC=0x15, dispatch through a syscall table, and return cleanly with eret. Four real syscalls (write, exit, getpid and yield) and the hardware-enforced privilege boundary that makes them possible.

  11. 11

    Rust no_std Kernel Allocator: Bringing Rust Into a C Kernel

    Carve the C heap allocator out of PurgatoryOS and replace it with a no_std Rust implementation wired in via extern C FFI. Set up aarch64-unknown-none-softfloat, build-std core + alloc, the GlobalAlloc trait, a spin-locked free list, and a Makefile that links libpurgatory_rs.a into the existing kernel.elf, without rewriting a single line of C above the allocator.

  12. 12

    Build a Simple Filesystem: Turning the Heap Into a Ramdisk

    Build an in-memory tree filesystem for PurgatoryOS in no_std Rust using Box<Node>, BTreeMap<String, NodeId>, and Vec<u8> on top of Post 11's allocator. Add three new syscalls (open, read, close), a per-task file descriptor table, and a working VFS layer that the shell in Post 13 can type against.

  13. 13

    Writing a Shell in C: A REPL Over the UART

    Give PurgatoryOS a prompt. Add uart_getc and a line-discipline buffer, parse commands on top of Post 12's ramdisk, dispatch built-ins (ls, cat, echo, cd, pwd, help, clear), and load /bin/hello as a flat binary at a fixed address. This is the payoff post, where readers finally type into their own kernel.

The series page, From Silicon to Shell: Building an OS for ARM and More , has the same chapters with a topic index.

Prerequisites

You need to be comfortable reading and writing basic C, which means pointers, structs, and the compile and link cycle. Everything else, from exception levels to page tables, is built up from first principles as we go.

The tooling is a cross-compiler and QEMU on macOS or Linux. Chapter 2 walks through the full toolchain setup, and your first ARM binary boots about fifteen minutes in.

If you would rather start with how the hardware executes instructions in the first place, From Transistor to System is the companion series on computer architecture.

Want the background first? The Operating Systems learning path puts this project in order with the chapters and explorers that lead up to it.

Verified toolchain

The versions the series' code was last tested with.

  • QEMU 9.x (qemu-system-aarch64, virt machine, cortex-a53)
  • aarch64-elf-gcc 14.x

Repository

Each chapter's code is on its own branch, so what you check out matches what you read.

github.com/nth4n-dev/PurgatoryOS

Branch per chapter (10)

Explorers for this project

Drive the hardest part of the project yourself before, or after, you implement it.

Operating Systems

Virtual Memory Explorer

Walk an AArch64 page table, watch the TLB fill and trigger page faults.

Frequently asked questions

Do I need real hardware to build an operating system?

No. The entire course runs on QEMU's virt machine, which emulates a full AArch64 system in software. You write, build and boot your kernel on macOS or Linux with nothing more than a cross-compiler and QEMU, and everything you learn transfers directly to real ARM boards like the Raspberry Pi.

How long does it take to build an OS from scratch?

Expect roughly 25 to 40 hours across 13 chapters, from first boot to an interactive shell. Each chapter is a self-contained milestone with 15 to 40 minutes of reading, plus the time you spend implementing the code and, inevitably, debugging it.

Can you write an operating system in Rust?

Yes. This course builds the kernel core in C and then brings in Rust through a no_std allocator wired in over the C FFI, which shows how the two languages can coexist in a single kernel image. A kernel written entirely in Rust is possible too, but the mixed approach mirrors how real systems adopt Rust in practice: one subsystem at a time.

What do I need to know before starting?

You should be comfortable reading and writing basic C, meaning pointers, structs and functions. No assembly, ARM or operating systems background is required, because exception levels, MMU configuration and context switching are all taught from first principles.

Why build for ARM64 instead of x86?

AArch64 has a much cleaner architecture, without the decades of legacy boot machinery that x86 drags along, so your time goes into real operating system concepts instead of historical workarounds. It is also the instruction set in every modern phone, every Apple Silicon Mac and a growing share of servers.

Is learning OS development still worth it?

Yes, although not because you will ship a commercial kernel. The value is that every abstraction you rely on daily, from processes and virtual memory to system calls and drivers, becomes concrete once you have built it yourself. It is some of the highest-leverage systems knowledge a software engineer can pick up.

Start at chapter one

Written by the engineers behind BIOS Confessions. 13 of 13 chapters are published, and all of them are free.

Begin with Chapter 1