5-Part Series:
- Part 0 (this): Why build an OS from scratch?
- Part 1: Foundations (coming soon)
- Part 2: Communication (coming soon)
- Part 3: Concurrency (coming soon)
- Part 4: Memory and beyond (coming soon)
GitHub Repository: bahree/rust-microkernel — full source code, build scripts, and Docker image
Why this, why now?
I recently wrapped up an incredible chapter at Microsoft, working on the AI engineering team in CoreAI. Between roles, I found myself on gardening leave with something rare in this industry: a few weeks of unstructured time. And instead of doing the sensible thing (rest, catch up on sleep, maybe touch grass), my brain went: “You know what sounds fun? Writing an operating system from scratch.”
Look, I’ve spent the last several years deep in AI/ML, from foundational model development to applied AI at scale. That work isn’t going anywhere (the LLM from scratch series on this blog is a good example of that curiosity). But I’ve had this itch for a while now to go back to the fundamentals. The stuff that sits beneath all those PyTorch tensors and CUDA kernels. The stuff that actually makes a computer compute.
There’s an old joke: a QA engineer walks into a bar. Orders 1 beer. Orders 0 beers. Orders 99999999 beers. Orders -1 beers. Orders a lizard. Orders NULL beers. First real customer walks in and asks where the bathroom is. The bar bursts into flames.
That’s basically what happens when you try to run code without an OS. You think you’re ordering a beer, but there’s nobody behind the counter, no glasses, no refrigerator, and the building doesn’t have plumbing yet. You are the plumbing. This series is about building that plumbing from nothing.
Part 0? Really?
Yes, we’re starting at zero. Because real programmers count from zero, and honestly, this part is about understanding what we’re building before we build it. Think of it as the moment before you start cooking, where you read the whole recipe, check you’ve got the ingredients, and figure out what you’re actually making. You don’t skip that step. (Okay, maybe you do sometimes. But you shouldn’t.)
If you’ve ever wondered how code even runs without an operating system underneath, or what happens between pressing the power button and seeing a login screen, or why everyone says “virtual memory” but nobody explains how it actually works, you’re in the right place.
What happens when there’s nothing underneath you?
Every program you’ve ever written ran on top of an operating system. When you call println!, the OS figures out where to send those bytes. When you allocate memory, the OS finds a free chunk and maps it into your address space. When your code runs alongside other programs without crashing into them, that’s the OS keeping everyone in their own lane.
Now imagine none of that exists. No OS. No standard library. No heap. No println!. Just you, a CPU, and some RAM. You want to print a single character? You need to know the exact memory address of the serial port hardware, write one byte to that address, and hope you configured the device correctly. If you didn’t, nothing happens. The screen stays blank. The CPU doesn’t complain. It just sits there.
That’s where we’re starting. We’re going to build, from nothing, the thing that makes all of those invisible layers work. By the end, you’ll understand exactly what happens between “power on” and “your code runs.” And honestly? It’s one of the most satisfying things you can do as a programmer.
TL;DR
This is a 5-part educational series (Part 0 through Part 4) where we build a minimal OS kernel in Rust from scratch. We focus on AArch64 QEMU virt, which means anyone with a laptop can run it. No special hardware needed, just QEMU.
Here’s what you’ll learn:
- Boot sequences: How a CPU goes from power-on to running your first line of Rust
- Serial output: The simplest possible way to see what your kernel is doing
- Message-passing IPC: How tasks communicate without sharing memory
- Cooperative and preemptive scheduling: From polite turn-taking to the OS forcibly yanking the CPU away
- Timer interrupts: How hardware gives your OS a sense of time
- Context switching: Saving and restoring every register so tasks don’t know they were paused
- Virtual memory: Page tables, the MMU, and why every address your code uses is a lie
Warning: This is a learning project, not production code. The goal is understanding, not security or completeness.
Time commitment: About 2-4 hours per part, totaling 5 parts. Follow along at your own pace.
1. A quick primer: AArch64, QEMU, and the “virt” machine
You’ll see the phrase “AArch64 QEMU virt” throughout this series, so let’s unpack it before we go further.
AArch64 is ARM’s 64-bit instruction set architecture. It’s the CPU architecture inside your phone, your tablet, Apple Silicon Macs, most cloud servers (AWS Graviton, Ampere Altra), and the Raspberry Pi 3 and newer. When we say we’re writing an AArch64 kernel, we mean we’re writing code that speaks the language these CPUs understand: A64 instructions, 31 general-purpose 64-bit registers (x0 through x30), four exception levels (EL0 through EL3) for privilege separation, and a specific way of handling interrupts, memory, and device access.
If you’ve only worked with x86_64 (Intel/AMD), the concepts are the same, but the details differ. ARM uses a load/store architecture (you can’t perform arithmetic directly in memory; you have to load values into registers first), a different interrupt model (GIC instead of APIC), and a different boot flow (exception levels instead of real/protected/long mode transitions). None of this matters yet. We’ll explain every ARM-specific detail as we encounter it.
QEMU is a machine emulator. It simulates an entire computer in software: CPU, RAM, interrupt controller, timer, serial port, everything. When we run our kernel in QEMU, it doesn’t touch your real hardware. QEMU pretends to be an ARM computer, and our kernel runs on it. This is incredibly useful for OS development because you can restart instantly, you can’t brick anything, and the emulated hardware behaves predictably.
“virt” is a specific machine type provided by QEMU. Real ARM hardware comes in thousands of configurations: different board layouts, different peripherals, different memory maps. The virt machine is a clean, minimal design created by QEMU specifically for virtual machines and testing. It gives you:
- A PL011 UART (serial port) at a known memory address for text output
- A GICv2 interrupt controller for routing hardware interrupts to the CPU
- An ARM Generic Timer for periodic tick interrupts
- RAM starting at a fixed address (0x40000000)
- No legacy baggage, no quirky firmware, no board-specific workarounds
This is why we chose it for the blog series. The virt machine strips away all the hardware complexity that has nothing to do with OS fundamentals. You don’t need to worry about GPU initialization, USB enumeration, or PCIe configuration. You get a CPU, memory, a timer, an interrupt controller, and a serial port. That’s exactly what you need to learn how an OS works, and nothing more.
When you run qemu-system-aarch64 -machine virt, you’re telling QEMU: “pretend to be this specific kind of ARM computer.” Our kernel is written to match that pretend computer’s memory map and peripherals.
2. What we’re building
rustOS is a minimal microkernel. Not a “toy” that prints hello world and stops, but not Linux either. It’s the kind of thing that actually boots, actually runs multiple tasks, actually translates virtual addresses to physical ones. The interesting middle ground where you learn how real operating systems work without drowning in 30 million lines of code.
The kernel boots on AArch64 QEMU virt (an emulated ARM machine that anyone can run). It implements message-passing IPC, the core idea behind microkernel design, in which tasks communicate by sending small, fixed-size messages through a router rather than poking each other’s memory directly. It runs a cooperative scheduler that polls tasks in round-robin, and then we upgrade to preemptive multitasking with timer interrupts so tasks can’t hog the CPU. And it manages virtual memory with 4-level page tables and a real MMU, the hardware that makes every modern OS possible.
Now, the codebase also supports x86_64 and the Raspberry Pi Zero 2 W. Those platforms share the same kernel logic and are available in the repository if you want to explore them. But the blog series focuses on AArch64 virt because it’s the most accessible. You don’t need a Raspberry Pi. You don’t need a Windows machine to deal with x86 bootloader quirks. You need QEMU, and QEMU runs on everything.
Before we look at the code layout, a quick Rust concept if you’re coming from another language: a crate is Rust’s unit of compilation, roughly equivalent to a library or package in other ecosystems. A crate has its own Cargo.toml (think package.json or pom.xml) that declares its name, dependencies, and build settings. A workspace groups multiple crates together so they can share dependencies and be built in one cargo build invocation. Our project is a workspace with several crates: kernel, hal, and one per platform. Each crate compiles independently and can depend on others. When you see kernel crate or hal crate below, think “self-contained module with a clear API boundary.”
Here’s how the code is organized:
graph TB
subgraph "Platform-Agnostic Core"
kernel[kernel<br/>IPC Router, Scheduler, Tasks]
hal[hal<br/>Logger Trait, Arch Primitives]
end
subgraph "Platform Crate"
virt[arch_aarch64_virt<br/>QEMU Boot, PL011 UART<br/>Timer, GIC, MMU]
end
virt --> hal
virt --> kernel
style kernel fill:#e1f5ff
style hal fill:#e1f5ff
style virt fill:#fff4e1The kernel crate knows nothing about hardware. It talks to the world through a Logger trait defined in hal. The platform crate (arch_aarch64_virt) implements that trait using a PL011 UART driver and handles all ARM-specific setup: boot assembly, timer configuration, GIC interrupt routing, and MMU page tables. This separation is the same pattern you’ll find in real operating systems. Linux has arch/arm64/ for ARM-specific code and kernel/ for the portable parts. We’re doing the same thing, just at a much smaller scale.
3. Why this series?
Most OS tutorials either skip the hard parts (boot, interrupts, memory management), use simulators instead of real code, or don’t explain why things work the way they do. You get a code dump with a comment saying “configure the GIC” and zero explanation of what the GIC is, why it exists, or what happens if you get it wrong.
This series is different. We show complete implementations with no hand-waving. Every register write gets explained. Every design decision has a rationale. When we configure the MMU, we don’t just say “set TCR_EL1 to this magic number.” We break down every bit field, explain what it controls, and why we chose that value.
We also focus on understanding the features. A production OS needs thousands of things we don’t build: filesystems, networking, user mode, multicore support, security hardening. We skip all of that. Not because those things aren’t important, but because they all build on the foundation we’re teaching here. You can’t write a filesystem without understanding memory management. You can’t implement a network stack without interrupts. Get the foundation right, and the rest is (hard, but conceptually clear) engineering.
And we’re using Rust, which honestly makes bare-metal programming less terrifying than it used to be. The type system catches entire classes of bugs at compile time. You’ll still write plenty of unsafe code (this is an OS, after all), but the safe parts of Rust keep you honest about where the dangerous bits are.
Operating systems sit at the intersection of hardware (registers, interrupts, memory controllers), architecture (x86 vs ARM, privilege levels, MMUs), concurrency (multiple tasks, race conditions), and abstraction (clean APIs over messy hardware). There’s no debugger, no standard library, and mistakes cause silent crashes. This series shows you how to navigate that complexity, one piece at a time.
4. Why Rust? (and not C or assembly)
If you’re building an OS, you’re probably wondering: why Rust? Traditionally, kernels are written in C (Linux, xv6, Minix) or even raw assembly. Both are fine choices. But Rust brings something genuinely new to the table.
4.1 The problem with C
C gives you total control. No garbage collector, no runtime, direct memory access. That’s why it’s been the lingua franca of OS development for 50 years. But C also trusts you completely, and that trust is the problem. Buffer overflows, use-after-free, null pointer dereferences, data races: these aren’t theoretical. They’re the source of roughly 70% of security vulnerabilities in production systems (Microsoft and Google have both published studies confirming this).
In kernel code, these bugs are catastrophic. A buffer overflow in userspace causes your program to crash. A kernel-space buffer overflow corrupts the entire system. There’s no safety net below you.
4.2 The problem with assembly only
You could write an OS in pure assembly. Some people do, and there’s real value in understanding every instruction. But assembly doesn’t scale. An OS needs abstractions (traits, modules, type checking) to stay maintainable. Writing a mailbox router in assembly would work, but debugging it would be miserable. And you’d lose all the benefits of a compiler that can optimize, inline, and catch type errors for you.
4.3 What Rust gives you
Rust’s ownership system catches entire classes of bugs at compile time. You can’t use a value after freeing it. You can’t have two mutable references to the same data. You can’t forget to initialize memory. The borrow checker is annoying until you realize it’s catching bugs that would take hours to debug on bare metal, where there’s no debugger, no address sanitizer, and no core dump.
But Rust also gets out of your way when you need it to. The unsafe keyword lets you write raw pointer dereferences, inline assembly, and MMIO operations. You have to be explicit about it. The dangerous parts are clearly marked, and the safe parts are genuinely safe. This is a huge improvement over C, where everything is implicitly unsafe.
Rust’s no_std ecosystem is also mature. Crates like bootloader, uart_16550, and volatile provide building blocks for OS development. The core library gives you basic types, traits, and iterators without needing an allocator. And #[repr(C)] lets you control struct layout for hardware register interactions.
Concretely, here are bugs Rust prevented during this project:
- The compiler refused to let us access the IPC router from an interrupt handler without explicit synchronization (
UnsafeCell+Syncimpl) - A missing
volatileread would have been optimized away in C, silently breaking UART output. In Rust, we had to useread_volatile, making the intent clear explicitly - Type mismatches between page table entry formats were caught at compile time, not at boot time, with a mysterious hang
Is Rust perfect for OS development? No. The borrow checker sometimes fights you in ways that feel unnecessary for single-threaded kernel code. static mut is awkward. Some patterns that are natural in C require contortions in Rust. But on balance, we think the trade-off is overwhelmingly positive. You spend a little more time satisfying the compiler, and a lot less time debugging silent memory corruption.
5. Getting started
You’ll need Rust (nightly), QEMU, and basic Rust knowledge. If you know what ownership and traits are, you’re good. If you’ve written a function that takes &self and returned a Result, you’ll be fine. You don’t need to know assembly or have prior OS background. That’s the whole point of the series.
| Tool | Purpose | Installation |
|---|---|---|
| Rust (nightly) | Bare-metal compilation | rustup default nightly |
| QEMU | Virtual machine for running our kernel | apt install qemu-system-aarch64 or brew install qemu |
| Git | Cloning the repository | You probably already have this |
5.1 Install Rust (nightly)
# Install Rust via rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Follow prompts, select default installation
# Then reload your shell environment:
source $HOME/.cargo/env
# Switch to nightly (required for bare-metal features)
rustup default nightly
# Add the rust-src component (needed for building core for bare-metal targets)
rustup component add rust-src --toolchain nightly
# Add the AArch64 bare-metal target
rustup target add aarch64-unknown-noneVerify it works:
$ rustc --version
rustc 1.86.0-nightly (5765cbc51 2025-01-28)
$ rustup target list --installed | grep aarch64
aarch64-unknown-none5.2 Install QEMU
# Ubuntu/Debian
sudo apt install qemu-system-aarch64
# macOS
brew install qemu
# Verify
$ qemu-system-aarch64 --version
QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.4)5.3 Clone and build
$ git clone https://github.com/bahree/rust-microkernel.git
$ cd rust-microkernel
$ ./scripts/build-aarch64-virt.sh
[virt] building aarch64 QEMU virt ELF (default features)...
Finished `release` profile [optimized] target(s) in 0.28s
[virt] wrote dist/virt/os-aarch64-virt.elf5.4 Run it
$ ./scripts/run-aarch64-virt.sh
rustOS: aarch64 QEMU virt boot OK
rustOS: memory management demo (frames + page tables)
mm: demo start
mm: kernel_end=0x000000004009A010
mm: free_start=0x000000004009B000
mm: ram_end=0x0000000050000000
mm: frame0=0x000000004009B000
mm: frame1=0x000000004009C000
mm: read0=0x00000000AABBCCDD
mm: read1=0x0000000011223344
mm: ttbr0=0x0000000040085000
mm: test_va=0x0000000080000000
mm: enabling MMU (caches off)...
mm: test_va_read=0x00000000DEADBEEF
mm: demo done (MMU is ON)Press Ctrl+A then X to exit QEMU.

If you see that output, congratulations. You just ran an operating system kernel that booted on bare metal, allocated physical memory frames, built 4-level page tables, enabled the MMU, and successfully translated a virtual address from a few shell commands.
5.5 Try the other demos
The codebase has multiple demos you can build with feature flags:
# IPC + cooperative scheduling (Part 2)
$ ./scripts/build-aarch64-virt.sh demo-ipc && ./scripts/run-aarch64-virt.sh
rustOS: aarch64 QEMU virt boot OK
rustOS: IPC + cooperative scheduling demo
rustOS: kernel online
rustOS: microkernel step 1 (IPC + cooperative scheduling)
sched: starting
task/ping: poll
task/ping: sent ping
task/pong: got ping
# Preemptive multitasking (Part 3)
$ ./scripts/build-aarch64-virt.sh demo-preempt && ./scripts/run-aarch64-virt.sh
rustOS: aarch64 QEMU virt boot OK
rustOS: preemptive multitasking demo
A
B
A
B
A
B
...

If all three demos work, your environment is fully set up and ready for the series.
5.6 Docker (no local setup)
If you just want to try it without installing anything:
docker pull amitbahree/rust-microkernel:latest
docker run -it amitbahree/rust-microkernel:latest
# Inside the container:
./scripts/build-aarch64-virt.sh && ./scripts/run-aarch64-virt.shx86_64 and Raspberry Pi builds also exist in the repository. Run ./scripts/build-x86.sh or ./scripts/build-rpi.sh if you want to try them. They share the same kernel logic but use different boot code and hardware drivers.
6. Series roadmap
Here’s what each part covers and why it comes in that order. Each part builds on the previous one. You can’t preempt tasks without interrupts. You can’t isolate processes without virtual memory. The ordering isn’t arbitrary.
6.0 Part 0: Why build an OS from scratch? (you are here)
The overview. What we’re building, why, and how the pieces fit together. You’re reading it right now, so you’re already ahead.
6.1 Part 1: Foundations (boot process, assembly, UART, platform abstraction)
How a CPU goes from power-on to running your first line of Rust. We write AArch64 boot assembly that drops from EL2 (hypervisor) to EL1 (kernel), sets up a stack, zeroes BSS, and jumps to Rust code. We implement a PL011 UART driver so we can actually see output. And we establish the platform abstraction (HAL) that lets the kernel crate stay hardware-agnostic.
6.2 Part 2: Communication (IPC, task abstraction, cooperative scheduling)
Message-passing IPC with a mailbox router. Tasks implement a poll() method, and the scheduler calls them in a round-robin fashion. We build PingTask and PongTask to demonstrate communication. And we talk about why cooperative scheduling has limits (one misbehaving task freezes the system).
6.3 Part 3: Concurrency (interrupts, timers, context switching, preemption)
This is the big one. We configure the ARM Generic Timer and the GICv2 interrupt controller to get periodic timer interrupts. Then we build a preemptive scheduler that saves all 31 general-purpose registers plus SP, ELR, and SPSR, switches to a different task’s saved state, and returns from the exception as if nothing happened. Two threads print “A” and “B” without ever yielding, proving preemption works.
6.4 Part 4: Memory and beyond (frame allocator, page tables, MMU, next steps)
Virtual memory from the ground up. We build a bump allocator for physical frames, construct 4-level page tables (L0/L1/L2/L3), configure the MMU with MAIR, TCR, and TTBR0 registers, and enable address translation. Then we verify it works by writing through a virtual address and reading back through the physical one. The post ends with reflections on what we built, how it compares to real OSes, and where to go next.
7. The big picture: what makes an operating system?
Before we start building, let’s understand what we’re aiming for. An operating system has many layers, and we’re focusing on the foundational ones that everything else depends on.
7.1 Complete OS architecture
graph TB
subgraph "User Space (Ring 3 / EL0)"
Apps[Applications<br/>ls, cat, browser, games]
Libs[Standard Libraries<br/>libc, libstd]
Shell[Shell<br/>bash, zsh]
end
subgraph "Kernel Space (Ring 0 / EL1)"
subgraph "System Call Interface"
Syscalls[System Calls<br/>open, read, write, fork]
end
subgraph "Process Management"
Sched[Scheduler<br/>Round-robin, CFS, priorities]
IPC[IPC<br/>Message passing, pipes, signals]
Proc[Process Table<br/>PIDs, state, resources]
end
subgraph "Memory Management"
VMM[Virtual Memory<br/>Page tables, TLB, swapping]
Heap[Heap Allocator<br/>malloc/free, buddy system]
MMU[MMU Control<br/>TTBR, page faults]
end
subgraph "File Systems"
VFS[VFS Layer<br/>Common file API]
FS[File Systems<br/>ext4, FAT, NTFS]
Cache[Page Cache<br/>Read-ahead, write-back]
end
subgraph "Device Drivers"
Block[Block Devices<br/>Disk, SSD, NVMe]
Net[Network<br/>Ethernet, WiFi, TCP/IP]
Char[Character Devices<br/>UART, keyboard, mouse]
GPU[Graphics<br/>Framebuffer, GPU drivers]
end
subgraph "Core Kernel - WHAT WE BUILD"
Boot[Boot<br/>Platform init, entry]
Int[Interrupts<br/>Timer, exceptions, GIC/PIC]
Ctx[Context Switch<br/>Save/restore registers]
Mem[Physical Memory<br/>Frame allocator]
end
end
subgraph "Hardware"
CPU[CPU<br/>Cores, caches, MMU]
RAM[RAM<br/>Physical memory]
Disk[Storage<br/>HDD, SSD]
Devices[Peripherals<br/>UART, Timer, GPIO]
end
Apps --> Syscalls
Libs --> Syscalls
Shell --> Syscalls
Syscalls --> Sched
Syscalls --> VMM
Syscalls --> VFS
Sched --> Ctx
Sched --> Proc
Sched --> IPC
VMM --> MMU
VMM --> Heap
VMM --> Mem
VFS --> FS
VFS --> Cache
FS --> Block
Block --> Devices
Net --> Devices
Char --> Devices
GPU --> Devices
Int --> CPU
Ctx --> CPU
MMU --> CPU
Mem --> RAM
Boot --> CPU
style Boot fill:#e8f5e8
style Int fill:#e8f5e8
style Ctx fill:#e8f5e8
style Mem fill:#e8f5e8
style IPC fill:#fff4e1
style Sched fill:#fff4e1
style VMM fill:#fff4e1Look at that diagram. It’s a lot. But here’s the thing: we’re only building the green and yellow boxes at the bottom. Boot, interrupts, context switching, physical memory, IPC, scheduling, and virtual memory. That’s the core kernel layer, the absolute foundation that every OS needs. These aren’t optional features you can skip. They’re the bedrock on which everything else is built.
7.2 What we build (the foundation)
Boot sequences (Part 1) are how a computer goes from pressing the power button to running your kernel’s first line of code. It’s the handoff between hardware/firmware and your operating system. On ARM, this means handling exception levels (EL2 hypervisor to EL1 kernel), setting up a stack, zeroing the BSS, and enabling floating-point instructions before the LLVM-generated code can run. Get any step wrong, and the CPU hangs silently.
IPC and scheduling (Parts 2 and 3) are how multiple tasks run on a single CPU core. The scheduler decides who runs when. IPC lets tasks communicate without sharing memory. We start with cooperative scheduling (tasks voluntarily yield) and upgrade to preemptive scheduling (the OS is in control). This requires context switching: saving all 31 ARM registers, the stack pointer, the return address, and the CPU flags, then loading another task’s saved state.
Interrupts and exceptions (Part 3) are hardware signals that tell the CPU, “stop what you’re doing and handle this NOW.” The timer fires, interrupt! Without interrupts, your OS would have to constantly poll hardware (“Are you done yet? How about now?”), wasting billions of CPU cycles. And you’d have no way to switch between tasks forcibly. Interrupts are the foundation for everything asynchronous in a computer.
Memory management (Part 4) is the illusion that each program has its own private address space. The MMU (Memory Management Unit) translates every address using page tables, a 4-level tree structure that maps virtual addresses to physical ones. We build those tables, configure the MMU, and enable translation. After that, every memory access your code makes goes through hardware that we configured.
7.3 What we don’t build
We skip filesystems, networking, user mode, multicore, and security hardening. Not because they’re unimportant, but because they all build on the foundation we’re teaching. You can’t write a filesystem without memory management. You can’t implement a network stack without interrupts. You can’t add user mode without understanding privilege levels and page tables. Part 4 points you to resources for going deeper into each of these areas.
8. Repository structure
The full source code is at github.com/bahree/rust-microkernel . Here’s how it’s organized, focused on the AArch64 virt platform that the blog series covers:
rust-microkernel/
├── crates/
│ ├── kernel/ # Platform-agnostic kernel logic
│ │ ├── lib.rs # Entry point (kmain)
│ │ ├── ipc.rs # Message-passing router
│ │ └── sched.rs # Cooperative scheduler
│ ├── hal/ # Hardware abstraction layer
│ │ ├── log.rs # Logger trait
│ │ └── arch.rs # Architecture primitives (halt)
│ └── arch_aarch64_virt/ # AArch64 QEMU virt platform
│ ├── boot.S # Assembly: EL2→EL1 drop, vectors
│ ├── main.rs # Rust entry point
│ ├── uart.rs # PL011 UART driver
│ ├── timer.rs # Generic Timer + GIC
│ ├── preempt.rs # Context switching
│ └── mem.rs # Frame allocator + MMU
├── scripts/
│ ├── build-aarch64-virt.sh # Build AArch64 virt kernel
│ └── run-aarch64-virt.sh # Run in QEMU
├── docs/
│ └── blog/ # This blog series
└── dist/ # Build artifactsThe kernel crate knows nothing about hardware. It accepts a Logger trait object and calls .log() to print messages. The hal crate defines that trait. The arch_aarch64_virt crate implements it with a real UART driver and handles everything ARM-specific: boot assembly, timer setup, interrupt routing, page tables.
The x86_64 and Raspberry Pi platform crates (arch_x86_64, arch_aarch64_rpi) also exist in crates/ and follow the same pattern. They’re not covered in the blog series, but use the same kernel and hal crates.
9. References and acknowledgments
This project stands on the shoulders of giants. Philipp Oppermann’s “Writing an OS in Rust” showed that Rust OS development is not only possible but genuinely enjoyable. MIT’s xv6 demonstrated that you can teach OS internals with a clean, readable codebase. The OSDev community has been documenting hardware quirks and boot sequences for decades, and their wiki is invaluable.
For operating systems theory, Operating Systems: Three Easy Pieces
(free online) is the best introduction we’ve found. For ARM architecture specifics, the ARM Architecture Reference Manual
is the definitive reference (and it’s freely available). For bare-metal Rust patterns, the Embedded Rust Book
covers no_std development in depth.
We also learned from real microkernel projects: seL4 (formally verified, beautifully designed), Minix 3 (Tanenbaum’s vision of what microkernels should be), and the L4 family (proving that microkernel IPC can be fast).
5-Part Series:
- Part 0 (this): Why build an OS from scratch?
- Part 1: Foundations (coming soon)
- Part 2: Communication (coming soon)
- Part 3: Concurrency (coming soon)
- Part 4: Memory and beyond (coming soon)
GitHub Repository: bahree/rust-microkernel — full source code, build scripts, and Docker image