Project 4: The no_std Kernel Core (The Naked Machine)
Project 4: The no_std Kernel Core (The Naked Machine)
âThe best way to understand your tools is to understand what theyâre built from.â - Linus Torvalds
Project Metadata
- Main Programming Language: Rust
- Coolness Level: Level 5: Pure Magic (Super Cool)
- Difficulty: Level 4: Expert
- Knowledge Area: Embedded Systems / OS Development
- Time Estimate: 2 weeks
- Prerequisites: Basic Rust ownership, structs, and traits; familiarity with hexadecimal and binary
What You Will Build
A minimal bootable kernel in Rust that runs on bare metal (QEMU x86_64). You will disable the standard library, implement a custom panic handler, and write directly to the VGA text buffer. No operating system. No runtime. Just you and the hardware.
Deep Theoretical Foundation
Before writing a single line of code, you must understand the conceptual landscape you are entering. This section builds your mental model from the ground up.
The Rust Library Hierarchy: core, alloc, and std
Rustâs standard library is not a monolith. It is carefully stratified into three layers, each building on the previous:
+------------------------------------------------------------------+
| std |
| The "Full Experience" - Threads, Files, Networking, I/O |
| Requires: An Operating System with system calls |
+------------------------------------------------------------------+
|
| builds on
v
+------------------------------------------------------------------+
| alloc |
| Heap Allocation - Box, Vec, String, Arc, Rc |
| Requires: A global allocator (malloc/free equivalent) |
+------------------------------------------------------------------+
|
| builds on
v
+------------------------------------------------------------------+
| core |
| The Foundation - Option, Result, iterators, math, slices |
| Requires: NOTHING (no OS, no allocator, no runtime) |
+------------------------------------------------------------------+
Why this matters for your kernel:
When you write #![no_std], you are telling the compiler: âI am not running on an operating system. Do not assume I have a heap, files, threads, or any OS services. Give me only what I can use on bare metal.â
What you lose without std:
println!(requires stdout, which requires OS)String,Vec,Box(require heap allocation)std::thread(requires OS thread scheduler)std::fs,std::net(require OS file system and networking stack)std::io(requires OS I/O subsystem)- Panic backtraces (require unwinding, which needs OS support)
What you keep with core:
Option<T>andResult<T, E>(just enums!)- Iterators and iterator adapters
core::fmtfor formatting (but no default output destination)- Mathematical operations
- Slice operations (
&[T],&str) core::ptrfor raw pointer manipulationPhantomData,Copy,Clone, and other marker traitscore::memfor memory intrinsicscore::sync::atomicfor atomic operations
Book Reference: âThe Secret Life of Programsâ by Jonathan Steinhart, Chapter 5
The Boot Process: From Power Button to Your Code
When you press the power button, a precisely choreographed sequence begins. Understanding this sequence is crucial for writing kernel code:
+-------------------------------------------------------------------------+
| THE BOOT SEQUENCE |
+-------------------------------------------------------------------------+
[1] POWER ON
|
v
+-------------------+
| BIOS/UEFI | <- Firmware burned into ROM on the motherboard
+-------------------+ <- Runs first, initializes hardware, finds bootloader
|
| Loads first 512 bytes from disk (MBR) into 0x7C00
v
+-------------------+
| BOOTLOADER | <- GRUB, rEFInd, or your custom Stage 1/2
+-------------------+ <- Switches CPU from Real Mode (16-bit) to Protected/Long Mode (64-bit)
| <- Loads your kernel binary into memory
v <- Sets up initial page tables, GDT
+-------------------+
| YOUR KERNEL | <- Starts execution at _start entry point
+-------------------+ <- YOU ARE HERE
|
| Your kernel eventually starts...
v
+-------------------+
| init | <- First userspace process (on a full OS)
+-------------------+
The CPU Mode Transitions:
Real Mode (16-bit) Protected Mode (32-bit) Long Mode (64-bit)
| | |
| 8086 compatibility | Virtual memory available | 64-bit addresses
| 1MB address space | 4GB address space | 256TB address space
| No memory protection | Ring-based protection | Full protection
| | |
+---- BIOS runs here ----+ +---- Legacy systems ----+ +---- Modern 64-bit ----+
| | |
+------ Your bootloader handles these transitions ------+
Why you need a bootloader: Your Rust code compiles to 64-bit machine code, but the CPU starts in 16-bit Real Mode (for backward compatibility with the 1978 Intel 8086). Someone needs to:
- Switch the CPU to 64-bit Long Mode
- Set up initial page tables (required for Long Mode)
- Load your kernel binary from disk into memory
- Jump to your kernelâs entry point
We use the bootloader crate, which handles all of this for us.
Book Reference: âComputer Systems: A Programmerâs Perspectiveâ by Bryant & OâHallaron, Chapter 7
Memory-Mapped I/O (MMIO): Talking to Hardware
In a modern computer, hardware devices donât have their own instruction set. Instead, they are âmappedâ into the CPUâs address space. Writing to a specific memory address doesnât write to RAM; it sends a command to a device.
+-------------------------------------------------------------------------+
| x86 MEMORY MAP AT BOOT |
+-------------------------------------------------------------------------+
0xFFFFFFFF +------------------+
| High Memory |
| (Extended RAM) |
| |
0x100000 +------------------+ <- 1MB mark. Your kernel loads here.
| BIOS ROM |
0xF0000 +------------------+
| Video BIOS ROM |
0xC0000 +------------------+
| |
| VGA MEMORY |
0xB8000 +------------------+ <- VGA TEXT BUFFER (4000 bytes)
| |
0xA0000 +------------------+ <- 640KB mark ("640K ought to be enough")
| |
| Conventional |
| Memory (RAM) |
| |
0x00000 +------------------+
The VGA Text Buffer:
The VGA text buffer is a 2D array of characters mapped to physical address 0xB8000. It is 80 columns by 25 rows = 2000 characters. Each character takes 2 bytes:
VGA Text Buffer Entry (16 bits)
+--------+--------+
| Byte 0 | Byte 1 |
+--------+--------+
| ASCII | Attrib |
| Code | utes |
+--------+--------+
Attribute Byte:
Bit 7: Blink (or bright background, depending on mode)
Bits 6-4: Background color (0-7)
Bits 3-0: Foreground color (0-15)
Color Values:
0 = Black 4 = Red 8 = Dark Gray 12 = Light Red
1 = Blue 5 = Magenta 9 = Light Blue 13 = Pink
2 = Green 6 = Brown 10 = Light Green 14 = Yellow
3 = Cyan 7 = Light Gray 11 = Light Cyan 15 = White
Example: Writing âAâ in green on black at position (0, 0):
Address: 0xB8000 + (0 * 160) + (0 * 2) = 0xB8000
Write: [0x41, 0x0A] <- 0x41 = 'A', 0x0A = green (2) on black (0)
Memory at 0xB8000:
+------+------+------+------+------+------+------+------+
| 0x41 | 0x0A | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | ...
+------+------+------+------+------+------+------+------+
'A' green (empty screen positions...)
Why volatile writes are essential:
The compiler is very smart. If it sees you write to a memory location that you never read from, it might âoptimize awayâ the write. But when you write to the VGA buffer, you ARE reading from it; the monitor reads from it! The compiler doesnât know this.
// BAD: Compiler might optimize this away
let vga_buffer = 0xB8000 as *mut u8;
unsafe { *vga_buffer = 0x41; } // Compiler: "Nobody reads this. Skip it."
// GOOD: Compiler must perform the write
use core::ptr::write_volatile;
unsafe { write_volatile(vga_buffer, 0x41); } // Compiler: "I must do this."
Book Reference: âOperating Systems: Three Easy Piecesâ by Remzi & Andrea Arpaci-Dusseau, Chapter on Memory-Mapped I/O
Linker Scripts: Controlling Memory Layout
When you compile a normal program, the linker decides where to put your code and data in memory. But a kernel isnât a normal program. You must tell the linker exactly where everything goes because the bootloader will load your kernel to a specific address.
+-------------------------------------------------------------------------+
| KERNEL BINARY SECTIONS |
+-------------------------------------------------------------------------+
.text (Code)
Contains: Executable instructions
Loaded at: Usually 0x100000 (1MB mark in x86_64)
Permissions: Read + Execute
.rodata (Read-Only Data)
Contains: String literals, constants
Permissions: Read only
.data (Initialized Data)
Contains: Global variables with initial values
Permissions: Read + Write
.bss (Uninitialized Data)
Contains: Global variables that start as zero
Permissions: Read + Write
Note: Does NOT take space in the binary file; the loader zeros this region
+----------------+
| .text | <- Entry point (_start) is here
+----------------+
| .rodata | <- "Hello, World!" string is here
+----------------+
| .data | <- Initialized globals
+----------------+
| .bss | <- Stack pointer starts here (grows downward)
+----------------+
The _start entry point:
Every executable needs an entry point, a symbol that the loader jumps to. In standard Rust programs, this is handled by the main function via the runtime. Without a runtime, you define it yourself:
#[no_mangle] // Don't mangle the name; linker needs to find "_start"
pub extern "C" fn _start() -> ! {
// Your kernel starts here
loop {} // Return type is `!` (never) because kernels don't return
}
Why extern "C"?
The bootloader is written in C (or assembly), so it expects the C calling convention. Rustâs default calling convention is not stable.
Why -> !?
A kernel never âreturns.â Thereâs nothing to return to! The ! type (called âneverâ) tells the compiler this function runs forever.
Book Reference: âComputer Systems: A Programmerâs Perspectiveâ by Bryant & OâHallaron, Chapter 7
The #[panic_handler] Attribute
In standard Rust, when a panic occurs, the runtime prints a message and unwinds the stack. But without std, there is no runtime. You must define what happens on panic:
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
// _info contains the panic message and location
// We could print to VGA here, but for now:
loop {} // Halt forever
}
What the panic handler receives:
PanicInfo structure:
+-------------------------------------------+
| message: Option<&Arguments> | <- The panic message
| location: Option<&Location> | <- File, line, column
+-------------------------------------------+
|
+-> Location {
file: &'static str, <- "src/main.rs"
line: u32, <- 42
column: u32, <- 5
}
Book Reference: âThe Rust Programming Languageâ, Chapter 9
Cross-Compilation and Target Specifications
Your development machine runs Linux, macOS, or Windows with an OS and a standard library. Your kernel runs on bare metal with neither. You must cross-compile.
The target triple:
x86_64-unknown-none
| | |
| | +-- OS (none = freestanding, no OS)
| +--------- Vendor (unknown = unspecified)
+--------------------- Architecture (x86 64-bit)
Custom target JSON:
For bare-metal kernels, we often create a custom target specification:
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
Key settings explained:
| Setting | Purpose |
|---|---|
panic-strategy: abort |
Donât try to unwind; just abort on panic |
disable-redzone |
The âred zoneâ is a 128-byte area below the stack that user programs can use. Interrupts would clobber it, so kernels disable it |
features: -mmx,-sse |
Disable SIMD instructions; kernels shouldnât use floating point without explicit setup |
Learning Objectives
By the end of this project, you will be able to:
- Explain the Rust library hierarchy (core, alloc, std) and what each layer requires
- Describe the x86 boot sequence from BIOS to kernel entry
- Implement a freestanding Rust binary without the standard library
- Write a custom panic handler that satisfies the Rust language requirement
- Use volatile writes to communicate with memory-mapped hardware
- Understand linker scripts and how they control binary layout
- Cross-compile Rust for a bare-metal target
- Debug a kernel using QEMU and serial port output
- Configure the VGA text buffer to display colored text
- Appreciate what the operating system does for you every day
Solution Architecture
Before coding, visualize what you are building:
Project Structure
nostd-kernel/
+-- .cargo/
| +-- config.toml <- Cross-compilation settings
+-- src/
| +-- main.rs <- Entry point and panic handler
| +-- vga_buffer.rs <- VGA text buffer abstraction
+-- Cargo.toml <- Dependencies (bootloader, volatile)
+-- x86_64-custom.json <- (Optional) Custom target spec
.cargo/config.toml
[unstable]
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]
[build]
target = "x86_64-unknown-none"
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
The Entry Point (_start)
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {
// Write "HELLO" to VGA buffer at 0xB8000
let vga_buffer = 0xb8000 as *mut u8;
let hello = b"HELLO";
for (i, &byte) in hello.iter().enumerate() {
unsafe {
core::ptr::write_volatile(vga_buffer.add(i * 2), byte);
core::ptr::write_volatile(vga_buffer.add(i * 2 + 1), 0x0b); // cyan
}
}
loop {}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
VGA Buffer Writer Struct
pub struct VgaWriter {
column: usize,
row: usize,
color: u8,
buffer: *mut u8,
}
impl VgaWriter {
pub const fn new() -> Self {
Self {
column: 0,
row: 0,
color: 0x0f, // white on black
buffer: 0xb8000 as *mut u8,
}
}
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column >= 80 {
self.new_line();
}
let offset = (self.row * 80 + self.column) * 2;
unsafe {
core::ptr::write_volatile(self.buffer.add(offset), byte);
core::ptr::write_volatile(self.buffer.add(offset + 1), self.color);
}
self.column += 1;
}
}
}
fn new_line(&mut self) {
self.column = 0;
self.row += 1;
if self.row >= 25 {
self.scroll();
}
}
fn scroll(&mut self) {
// Move all lines up by one
for row in 1..25 {
for col in 0..80 {
let src = ((row * 80 + col) * 2) as isize;
let dst = (((row - 1) * 80 + col) * 2) as isize;
unsafe {
let c = core::ptr::read_volatile(self.buffer.offset(src));
let a = core::ptr::read_volatile(self.buffer.offset(src + 1));
core::ptr::write_volatile(self.buffer.offset(dst), c);
core::ptr::write_volatile(self.buffer.offset(dst + 1), a);
}
}
}
// Clear the last row
for col in 0..80 {
let offset = (24 * 80 + col) * 2;
unsafe {
core::ptr::write_volatile(self.buffer.add(offset), b' ');
core::ptr::write_volatile(self.buffer.add(offset + 1), self.color);
}
}
self.row = 24;
}
}
Phased Implementation Guide
Phase 1: Create the Freestanding Binary
Goal: Get a Rust binary that compiles without the standard library.
Steps:
- Create a new project:
cargo new nostd-kernel - Add to
Cargo.toml:[package] name = "nostd-kernel" version = "0.1.0" edition = "2021" [profile.dev] panic = "abort" [profile.release] panic = "abort" - Replace
src/main.rs:#![no_std] #![no_main] use core::panic::PanicInfo; #[no_mangle] pub extern "C" fn _start() -> ! { loop {} } #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } - Create
.cargo/config.toml:[unstable] build-std = ["core", "compiler_builtins"] build-std-features = ["compiler-builtins-mem"] [build] target = "x86_64-unknown-none" - Build:
cargo build --release
Checkpoint: The build should succeed with no errors.
Phase 2: Set Up the Bootloader
Goal: Create a bootable image that QEMU can run.
Steps:
- Add dependencies to
Cargo.toml:[dependencies] bootloader = "0.9" [dependencies.bootloader] version = "0.9" features = [] - Install
bootimage:cargo install bootimage - Install QEMU:
- macOS:
brew install qemu - Ubuntu:
apt install qemu-system-x86
- macOS:
- Add runner to
.cargo/config.toml:[target.'cfg(target_os = "none")'] runner = "bootimage runner" - Build the bootable image:
cargo bootimage - Run in QEMU:
qemu-system-x86_64 -drive format=raw,file=target/x86_64-unknown-none/debug/bootimage-nostd-kernel.bin
Checkpoint: QEMU should boot and display a blank screen (no crash).
Phase 3: Write to the VGA Buffer
Goal: Display text on the screen without any library support.
Steps:
- Update
_start:#[no_mangle] pub extern "C" fn _start() -> ! { let vga_buffer = 0xb8000 as *mut u8; for (i, &byte) in b"RUST KERNEL LIVES!".iter().enumerate() { unsafe { core::ptr::write_volatile(vga_buffer.add(i * 2), byte); core::ptr::write_volatile(vga_buffer.add(i * 2 + 1), 0x0a); // green } } loop {} } - Rebuild and run
Checkpoint: âRUST KERNEL LIVES!â appears in green at the top-left.
Phase 4: Create the VGA Writer Abstraction
Goal: Build a reusable writer that handles newlines and scrolling.
Steps:
- Create
src/vga_buffer.rswith theVgaWriterstruct from the solution architecture - Add
mod vga_buffer;tomain.rs - Implement
core::fmt::WriteforVgaWriter:use core::fmt::{self, Write}; impl Write for VgaWriter { fn write_str(&mut self, s: &str) -> fmt::Result { for byte in s.bytes() { self.write_byte(byte); } Ok(()) } } - Use
write!macro in_start:use core::fmt::Write; let mut writer = vga_buffer::VgaWriter::new(); write!(writer, "Welcome to RustOS v0.1\n").unwrap(); write!(writer, "Memory: {} bytes\n", 128 * 1024 * 1024).unwrap();
Checkpoint: Formatted output with numbers works correctly.
Phase 5: Implement a Better Panic Handler
Goal: Display panic messages on screen instead of just halting.
Steps:
- Create a static global writer (requires
spincrate for mutex):# Cargo.toml [dependencies] spin = "0.9" - Create a global writer:
use spin::Mutex; use lazy_static::lazy_static; lazy_static! { pub static ref WRITER: Mutex<VgaWriter> = Mutex::new(VgaWriter::new()); } - Update panic handler:
#[panic_handler] fn panic(info: &PanicInfo) -> ! { use core::fmt::Write; let mut writer = crate::vga_buffer::WRITER.lock(); writer.set_color(0x4f); // white on red write!(writer, "\n!!! KERNEL PANIC !!!\n").unwrap(); if let Some(location) = info.location() { write!(writer, " at {}:{}\n", location.file(), location.line()).unwrap(); } loop {} }
Checkpoint: panic!("test") displays a red error message.
Phase 6: Add Serial Port Output (For Debugging)
Goal: Output text to QEMUâs serial console for easier debugging.
Steps:
- Add the
uart_16550crate:[dependencies] uart_16550 = "0.2" - Create
src/serial.rs:use uart_16550::SerialPort; use spin::Mutex; use lazy_static::lazy_static; lazy_static! { pub static ref SERIAL: Mutex<SerialPort> = { let mut port = unsafe { SerialPort::new(0x3F8) }; port.init(); Mutex::new(port) }; } #[macro_export] macro_rules! serial_println { () => ($crate::serial_print!("\n")); ($($arg:tt)*) => ($crate::serial_print!("{}\n", format_args!($($arg)*))); } #[macro_export] macro_rules! serial_print { ($($arg:tt)*) => { $crate::serial::_print(format_args!($($arg)*)); }; } pub fn _print(args: core::fmt::Arguments) { use core::fmt::Write; SERIAL.lock().write_fmt(args).unwrap(); } - Run QEMU with serial output:
qemu-system-x86_64 -drive format=raw,file=bootimage.bin -serial stdio
Checkpoint: serial_println!("Debug message") appears in your terminal.
Testing Strategy
Unit Tests in Hosted Environment
Create a separate library crate that can be tested on your host machine:
// src/lib.rs (compiled for host)
#[cfg(test)]
mod tests {
#[test]
fn test_vga_offset_calculation() {
let row = 5;
let col = 10;
let offset = (row * 80 + col) * 2;
assert_eq!(offset, 820);
}
#[test]
fn test_color_byte() {
let fg = 0x0a; // green
let bg = 0x00; // black
let color = (bg << 4) | fg;
assert_eq!(color, 0x0a);
}
}
Run with: cargo test --target x86_64-unknown-linux-gnu
Integration Tests with QEMU
Create a test framework that uses QEMUâs exit device:
// tests/should_panic.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = "test_main"]
#[no_mangle]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
}
exit_qemu(QemuExitCode::Success);
}
#[test_case]
fn trivial_assertion() {
assert_eq!(1, 1);
}
Serial Port Debugging
The serial port is your primary debugging tool. Use it liberally:
serial_println!("[DEBUG] Entering function foo");
serial_println!("[DEBUG] value = {:#x}", value);
Common Pitfalls and Debugging
Pitfall 1: Forgetting #![no_main]
Symptom: Linker error about undefined reference to main
Solution: Add #![no_main] to your crate root
Pitfall 2: Missing #[no_mangle] on _start
Symptom: Kernel boots but immediately crashes or does nothing
Solution: Add #[no_mangle] to preserve the _start symbol name
Pitfall 3: Not Using Volatile Writes
Symptom: VGA writes work in debug mode but not in release mode
Solution: Use core::ptr::write_volatile instead of *ptr = value
Pitfall 4: Stack Overflow Before Memory is Set Up
Symptom: Immediate crash or strange behavior at boot
Solution: Keep _start simple; avoid large stack allocations before initializing the allocator
Pitfall 5: Forgetting Panic = Abort
Symptom: Linker errors about eh_personality or unwinding symbols
Solution: Add panic = "abort" to [profile.dev] and [profile.release] in Cargo.toml
Pitfall 6: Red Zone Not Disabled
Symptom: Random crashes when interrupts fire
Solution: Use a target that disables the red zone, or add "disable-redzone": true to your custom target JSON
Pitfall 7: Using Floating Point Without Setup
Symptom: Illegal instruction exception when using f32 or f64
Solution: Disable SSE in your target: "features": "-mmx,-sse,+soft-float", or explicitly enable and save/restore SSE state
Extensions and Challenges
Once your basic kernel works, try these extensions:
Extension 1: Interrupt Handling
Set up the Interrupt Descriptor Table (IDT) to handle:
- Page faults (youâll need these for memory management)
- Timer interrupts (for preemptive multitasking)
- Keyboard interrupts (for input)
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt
};
}
extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {
serial_println!("BREAKPOINT\n{:#?}", stack_frame);
}
Extension 2: Basic Heap Allocator
Implement a simple bump allocator:
use alloc::alloc::{GlobalAlloc, Layout};
struct BumpAllocator {
heap_start: usize,
heap_end: usize,
next: AtomicUsize,
}
unsafe impl GlobalAlloc for BumpAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
// Align next pointer
// Bump and return
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
// Bump allocators don't deallocate individually
}
}
#[global_allocator]
static ALLOCATOR: BumpAllocator = BumpAllocator::new();
Extension 3: Keyboard Driver
Read scan codes from the PS/2 keyboard controller:
use x86_64::instructions::port::Port;
fn read_scancode() -> u8 {
let mut port = Port::new(0x60);
unsafe { port.read() }
}
Extension 4: Simple Shell
Once you have keyboard input, create a command prompt:
RustOS> help
Available commands:
help - Show this help
clear - Clear the screen
reboot - Reboot the system
RustOS> _
Real-World Connections
Your work connects to real operating systems and projects:
Redox OS
A Unix-like operating system written entirely in Rust. Studies in microkernel architecture.
- Website: https://www.redox-os.org/
- Source: https://gitlab.redox-os.org/redox-os/redox
Theseus OS
A research OS exploring new OS structuring techniques using Rustâs type system.
- Paper: âTheseus: An Experiment in Operating System Structure and State Managementâ
- Source: https://github.com/theseus-os/Theseus
Writing an OS in Rust (Blog)
Philipp Oppermannâs excellent tutorial series that goes far deeper than this project.
- URL: https://os.phil-opp.com/
- This is the definitive resource for continuing your OS journey.
Linux Kernel Rust Support
As of Linux 6.1, Rust is an officially supported language for kernel modules.
- Documentation: https://docs.kernel.org/rust/
The Interview Questions They Will Ask
- âWhat is the difference between
coreandalloccrates?âcoreprovides types and traits that require no runtime support.allocadds heap-allocated types likeBox,Vec,Stringbut requires a global allocator.
- âHow do you implement a global allocator in a no_std environment?â
- Implement the
GlobalAlloctrait withallocanddeallocmethods. - Mark your allocator with
#[global_allocator]. - The allocator manages a memory region (usually defined by the bootloader).
- Implement the
- âWhat is the purpose of the
#[panic_handler]attribute?â- It marks the function the compiler should call when a panic occurs.
- Without
std, thereâs no default; you must provide one. - It must diverge (return
!) because panics donât return.
- âWhat is a âfreestandingâ binary?â
- A binary that doesnât depend on an OS or runtime.
- It defines its own entry point, panic handler, and memory management.
- Itâs compiled with
#![no_std]and#![no_main].
- âWhy do we need volatile writes for memory-mapped I/O?â
- The compiler optimizes away writes it thinks are unused.
- Memory-mapped I/O âreadsâ happen via hardware, not code.
write_volatileprevents the optimization and guarantees the write occurs.
Real World Outcome
When complete, you will have:
$ cargo bootimage
Compiling nostd-kernel v0.1.0
Finished release [optimized] target(s) in 2.34s
Building bootable disk image...
Created bootimage
$ qemu-system-x86_64 -drive format=raw,file=target/x86_64-unknown-none/release/bootimage-nostd-kernel.bin
[QEMU Window Opens]
+------------------------------------------------------------------+
| RUST KERNEL v0.1.0 |
| Hardware initialized. |
| Memory: 128MB detected. |
| VGA Buffer Initialized at 0xb8000. |
| Writing 'Hello, World!' to screen... |
| > _ |
| |
| |
+------------------------------------------------------------------+
A .bin file that you can boot in QEMU, displaying your custom text on the screen without any OS running underneath. You are now one of the few developers who has written code that runs directly on the CPU with no operating system support.
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Startup & Linking | âComputer Systems: A Programmerâs Perspectiveâ | Ch. 7 |
| VGA & Hardware | âHow Computers Really Workâ | Ch. 8 |
| Embedded Rust | âThe Secret Life of Programsâ | Ch. 5 |
| OS Fundamentals | âOperating Systems: Three Easy Piecesâ | Part II |
Summary
This project strips away everything you take for granted, revealing the raw substrate upon which all software runs. You have learned:
- The Rust library hierarchy and what remains when you remove the OS
- The x86 boot sequence from power-on to kernel entry
- How to write code that runs with no runtime support
- Memory-mapped I/O and volatile access
- Cross-compilation for bare-metal targets
- The satisfaction of seeing YOUR code run on the naked machine
You are now equipped to understand operating systems at their most fundamental level. The next steps, interrupt handling, memory management, and process scheduling, will build on this foundation.
Welcome to the world of systems programming. The machine is now yours.