How Rust Rewrites Bootloaders Without Losing Its Soul
There’s something poetic about writing a bootloader in Rust. It’s like asking a poet to write machine code — in rhyme. A bootloader sits at the very edge of the known world — the first thing your CPU runs after power-on, before any OS, heap, or even std exists. It’s pure metal, pure chaos, and yet… Rust developers are somehow rewriting this primordial mess safely. But how? How can a language obsessed with ownership and safety fit inside a world that doesn’t even have memory yet? This is the story of how Rust went from being a “safe systems language” to running before any system even exists — and how it’s doing so without losing its soul. The Bootloader World: Where std Can’t Follow You A typical Rust binary depends on the standard library, which gives you niceties like Vec, String, thread, and file I/O. A bootloader, however, runs before any of that exists. When your CPU starts, it’s in real mode (on x86), executing raw machine instructions from a hardcoded address. There’s no heap, no syscalls, no OS — just bare instructions and a stack pointer. That’s why, to build bootloaders, you compile Rust with this tiny but magical configuration:
- ![no_std]
- ![no_main]
use core::panic::PanicInfo;
- [panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
- [no_mangle]
pub extern "C" fn _start() -> ! {
// Bootloader entry point
unsafe {
let vga_buffer = 0xb8000 as *mut u8;
*vga_buffer.offset(0) = b'H';
*vga_buffer.offset(1) = 0x0f; // white on black
}
loop {}
} This little snippet prints an H on screen — from scratch. No println!, no OS, no runtime. Just you, the CPU, and a pile of electrons. This is the Rust soul stripped bare. The Architecture: How Bootloaders Even Run Before we talk Rust internals, let’s look at the bootloader pipeline itself: [Power On]
↓
[BIOS / UEFI Firmware]
↓
[Bootloader Stage 1]
↓
[Bootloader Stage 2 (Rust!)]
↓
[Kernel] Stage 1 (often written in Assembly or C) gets control from the BIOS and loads Stage 2 into memory. That Stage 2 — the real bootloader logic — can now be written in Rust. Rust compiles down to position-independent, freestanding code that runs directly after the CPU initializes the segment registers and stack. The Rust compiler (LLVM) can emit code for that environment by targeting something like: --target x86_64-unknown-none This tells the compiler: “There’s no OS here. No syscalls. Just pure machine.” The Real Magic: Linking Without std When you remove std, Rust falls back to the core library — the minimal foundation of the language. It gives you basic types like:
- Option
- Result
- slice
- mem, ptr
- fmt (but limited)
Everything else — heap, I/O, threads — is gone. So, bootloader devs roll their own:
- Memory allocator? Custom bump or linked-list allocator.
- I/O? Direct VGA text memory writes or port I/O.
- Interrupts? Bare-metal ISR tables written in Rust or ASM.
Example: a VGA text driver written in Rust, no OS involved: use core::fmt::{self, Write}; use spin::Mutex;
const VGA_BUFFER: usize = 0xb8000;
const BUFFER_WIDTH: usize = 80;
const BUFFER_HEIGHT: usize = 25;
struct Writer {
col: usize, buffer: *mut u8,
} impl Writer {
fn write_byte(&mut self, byte: u8) {
unsafe {
*self.buffer.offset((self.col * 2) as isize) = byte;
*self.buffer.offset((self.col * 2 + 1) as isize) = 0x0f;
}
self.col += 1;
}
} impl Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
for byte in s.bytes() {
self.write_byte(byte);
}
Ok(())
}
} static WRITER: Mutex<Writer> = Mutex::new(Writer {
col: 0, buffer: VGA_BUFFER as *mut u8,
}); pub fn print(s: &str) {
use core::fmt::Write; let mut writer = WRITER.lock(); writer.write_str(s).unwrap();
} This looks so Rusty — and yet it’s bare metal. That’s the beauty: safety doesn’t disappear. It’s just built at a lower level. Architecture Design: The Layered Rust Bootloader A modern Rust bootloader (like bootloader) follows this design: ┌────────────────────────────┐ │ Bootloader Core (Rust, no_std) │ │ - Loads kernel │ │ - Switches to long mode │ │ - Sets up paging │ └──────────┬─────────────────┘
│
┌──────────┴─────────────────┐ │ Custom Drivers (Rust) │ │ - VGA text buffer │ │ - Disk / FAT reader │ │ - Memory map parser │ └──────────┬─────────────────┘
│
┌──────────┴─────────────────┐ │ Kernel (Rust + std opt.) │ │ - Starts userland / apps │ └────────────────────────────┘ Everything here — even paging setup — can be written in Rust using unsafe only where truly necessary. And that’s the key: Rust doesn’t eliminate unsafe — it contains it. Code Flow: From cargo run to Boot When you build your bootloader crate, you’re not producing a Linux binary. You’re producing a flat binary image the BIOS can load directly. Here’s what the flow looks like: cargo build --target x86_64-unknown-none
↓
rustc → LLVM → object file (.o)
↓
linker (ld) → stripped binary
↓
bootimage tool → adds boot header
↓
disk image (.bin / .iso)
↓
QEMU boots it When it boots, the CPU jumps straight to _start. No main, no runtime. The Real Reason Rust Fits Here You might ask: Why Rust? Why not just write bootloaders in Assembly or C? Because Rust gives something those can’t: safety boundaries that survive bare metal. Bootloaders are notorious for memory corruption, dangling pointers, and invalid memory maps. Rust doesn’t prevent all of that, but it dramatically reduces the chance you’ll shoot yourself in the foot while still letting you shoot directly at the hardware when you must. Plus, the ecosystem (like x86_64, bootloader, and uart_16550 crates) has matured enough to make low-level development not only possible but pleasant. Rust Without Losing Its Soul Rust’s core philosophy — “Fearless concurrency, fearless systems programming” — isn’t just for web servers. When Rust boots before any OS does, it’s not betraying that promise. It’s proving it. Because when you write:
- ![no_std]
- ![no_main]
you’re not losing features. You’re stepping into the purest form of Rust — where you decide what safety means. Key Takeaways
- Rust can run before the OS — in bootloaders, firmware, and even embedded systems.
- no_std and no_main allow full control over memory and startup.
- Safety doesn’t vanish; it’s layered on top of unsafe foundations.
- The Rust bootloader ecosystem (like bootloader crate) redefines what “safe low-level” means.
- Bootloaders written in Rust are more maintainable, testable, and robust — without losing speed.
Final Thoughts Rewriting bootloaders in Rust isn’t about hype or rewriting C for fun. It’s about proving a principle: that safety and control can coexist. Rust doesn’t need an OS to shine — it is the OS before the OS. So the next time your machine powers up, imagine a little Rust binary waking first, whispering: “Don’t worry, I got this.”
Read the full article here: https://medium.com/@theopinionatedev/how-rust-rewrites-bootloaders-without-losing-its-soul-4a94547c9c54
