Rust 121 🦀 Operating System Kernel Development Basics in no_std Rust
Writing an operating system kernel in Rust requires abandoning the comforts of the standard library. There is no main function injected by the runtime, no heap allocator provided by the system, no file system, and no underlying OS to handle panics or thread local storage. The kernel is the system. Rust’s no_std attribute is the gateway to this environment: it disables the standard library and leaves only core, a dependency-free subset that provides primitive types, Option, Result, and iterator traits . Building a kernel is the process of providing everything that std normally supplies, from the entry point to the panic handler to the interrupt descriptor table.
Key point: A no_std kernel is a binary that provides its own runtime. The #[panic_handler], the entry point (_start), and the memory layout are not optional—they are the minimum contract the compiler requires to produce a freestanding executable.
Why kernel development requires no_std
The runtime dependency problem. A standard Rust binary links against the C runtime (crt0) and the Rust runtime (std). The C runtime sets up the stack and passes command-line arguments; the Rust runtime initializes stack overflow guards and panic handling before calling main . A kernel runs before any of this exists. It cannot depend on crt0 or std because there is no operating system to provide them.
The unwinding problem. By default, Rust uses stack unwinding to run destructors during a panic. Unwinding requires OS-specific libraries like libunwind on Linux or structured exception handling on Windows . A kernel cannot use these. The solution is to set panic = "abort" in the Cargo profile, which disables unwinding and removes the eh_personality language item requirement .
The allocation problem. core does not include Vec, String, or Box. These types require a global allocator, which a kernel must implement itself (or defer until a heap is initialized). The alloc crate can be linked explicitly once an allocator is available, but the earliest boot stages must operate without heap allocation .
The target problem. The default Rust targets (like x86_64-unknown-linux-gnu) assume an OS and a C library. A kernel must compile for a bare-metal target. This is done either by using the built-in x86_64-unknown-none target or by defining a custom target specification JSON that sets llvm-target, panic-strategy, and disables the red zone .
The trade-off. no_std removes the safety net. Every panic, every memory allocation, and every hardware interaction must be explicitly handled. But this is the point: the kernel is the layer that provides safety and abstraction to everything above it.
a. Disabling std and providing the panic handler
The #![no_std] attribute at the crate root tells the compiler not to link the standard library. The core prelude replaces the std prelude, so Option and Result remain available without imports, but std::io, std::fs, and std::thread are gone .
Once std is disabled, the compiler requires a #[panic_handler]. This function is called whenever a panic occurs—an unwrap() on None, an out-of-bounds index, or an explicit panic!. In a kernel, the only safe behavior is to halt the CPU or reboot. An infinite loop is the simplest implementation, but a real kernel might print a diagnostic message to a serial port before halting.
#![no_std]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
// A real kernel would print to serial or VGA here.
loop {}
}
The #[panic_handler] attribute is mandatory for any no_std binary. Without it, the compiler produces a linker error stating that panic_impl is missing .
b. Defining the entry point and disabling unwinding
A no_std binary also lacks the start language item, which normally defines the entry point. The compiler’s default entry chain (crt0 → main) does not exist. The solution is to add #![no_main] and define an _start function with #[no_mangle] and extern "C" to prevent name mangling .
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
}
The panic = "abort" setting in Cargo.toml is required to disable unwinding. Without it, the compiler expects the eh_personality language item, which is provided by the standard library’s unwinding infrastructure. Setting it to abort removes this requirement and significantly reduces binary size .
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
c. Custom target specification and CPU features
For x86_64 kernel development, the built-in x86_64-unknown-none target works, but a custom target JSON provides finer control. The critical fields are llvm-target (set to x86_64-unknown-none), panic-strategy (set to abort), and disable-redzone (set to true) .
The red zone is a 128-byte region below the stack pointer that the System V ABI allows leaf functions to use without adjusting the stack pointer. In a kernel, an interrupt can occur at any time and overwrite this region, corrupting the interrupted function’s data. Disabling the red zone is mandatory for kernel stability .
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-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"
}
The features field disables MMX and SSE, which are problematic in a kernel because the compiler would need to save and restore the large SIMD register file on every interrupt. The +soft-float flag enables software floating-point emulation for code that needs it .
The .cargo/config.toml file sets the custom target as the default, so cargo build does not require an explicit --target flag. It also enables build-std to compile core and compiler_builtins from source for the custom target, since no precompiled versions exist for arbitrary targets .
[build]
target = "x86_64-blog_os.json"
[unstable]
build-std = ["core", "compiler_builtins"]
Complete Example Session
# ============================================
# PART 1: CARGO.TOML WITH PANIC ABORT
# ============================================
[package]
name = "kernel"
version = "0.1.0"
edition = "2021"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
# ============================================
# PART 2: MAIN.RS WITH NO_STD AND PANIC HANDLER
# ============================================
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
# ============================================
# PART 3: ENTRY POINT
# ============================================
#[no_mangle]
pub extern "C" fn _start() -> ! {
// Kernel code begins here.
loop {}
}
# ============================================
# PART 4: CUSTOM TARGET JSON
# ============================================
# x86_64-blog_os.json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
# ============================================
# PART 5: CARGO CONFIGURATION
# ============================================
# .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
[unstable]
build-std = ["core", "compiler_builtins"]
# ============================================
# PART 6: BUILD
# ============================================
cargo build
# Produces an ELF binary for the custom target.
# ============================================
# PART 7: VERIFY THE BINARY
# ============================================
file target/x86_64-blog_os/debug/kernel
# ELF 64-bit LSB executable, x86-64, statically linked
# ============================================
# PART 8: BOOT WITH QEMU
# ============================================
# Using bootimage or a bootloader crate:
cargo bootimage
qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-kernel.bin
# ============================================
# PART 9: SERIAL OUTPUT FOR DEBUGGING
# ============================================
use uart_16550::SerialPort;
static mut SERIAL: Option<SerialPort> = None;
#[no_mangle]
pub extern "C" fn _start() -> ! {
let mut serial = unsafe { SerialPort::new(0x3F8) };
serial.init();
// Now serial output can be used for debugging.
loop {}
}
The nine parts trace the minimum viable kernel: Cargo configuration, the no_std crate with panic handler, the entry point, the custom target specification, the build configuration, compilation, binary verification, QEMU boot, and serial output for debugging.
Quick Reference
Required Attributes and Items
| Item | Purpose |
|---|---|
#![no_std] | Disables standard library, links only core |
#![no_main] | Removes default entry point chain |
#[panic_handler] | Required for any no_std binary |
#[no_mangle] | Prevents name mangling of _start |
extern "C" | Ensures C ABI for entry point |
Cargo Settings
| Setting | Location | Value |
|---|---|---|
panic | Cargo.toml profiles | "abort" |
build-std | .cargo/config.toml | ["core", "compiler_builtins"] |
target | .cargo/config.toml | Path to custom JSON |
Target JSON Fields
| Field | Value | Purpose |
|---|---|---|
llvm-target | x86_64-unknown-none | Bare-metal x86_64 |
panic-strategy | abort | Disables unwinding |
disable-redzone | true | Prevents interrupt stack corruption |
features | -mmx,-sse,+soft-float | Disables SIMD, enables soft float |
Best Practices
✅ Do This:
#![no_std]
#![no_main]
- ✅ Both attributes are required for freestanding binaries
[profile.dev]
panic = "abort"
- ✅ Disable unwinding for kernel builds
"disable-redzone": true
- ✅ Mandatory for interrupt safety
❌ Don’t Do This:
#![no_std]
// missing panic handler
- ❌ Compiler error:
panic_implnot found
# Use the default x86_64-unknown-linux-gnu target
- ❌ Links against C runtime and libc
"disable-redzone": false
- ❌ Interrupts corrupt the red zone
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
eh_personality error | Unwinding enabled | Set panic = "abort" in Cargo.toml |
start lang_item missing | No entry point defined | Add #![no_main] and _start |
Linker errors for memcpy | Missing compiler_builtins | Enable build-std in Cargo config |
| QEMU boot fails | No bootloader or incorrect binary format | Use bootimage or a bootloader crate |
| Interrupts corrupt stack | Red zone enabled | Set disable-redzone: true |
Real-World Examples
1. Panic Handler
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
2. Entry Point
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
}
3. Disable Unwinding
[profile.release]
panic = "abort"
4. Custom Target
"llvm-target": "x86_64-unknown-none"
5. Disable Red Zone
"disable-redzone": true
6. Build Standard Library
[unstable]
build-std = ["core", "compiler_builtins"]
7. Serial Port Init
let mut serial = unsafe { SerialPort::new(0x3F8) };
serial.init();
8. Halt Loop
loop {
x86_64::instructions::hlt();
}
9. QEMU Run
qemu-system-x86_64 -drive format=raw,file=kernel.bin
10. Verify ELF
file target/x86_64-blog_os/debug/kernel
Visual
Freestanding Kernel Build Pipeline
┌──────────────────────────────────────────────┐
│ Rust source (no_std, no_main) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Cargo.toml: panic = "abort" │
│ Disables unwinding, removes eh_personality │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Custom target JSON │
│ disable-redzone: true │
│ panic-strategy: abort │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ cargo build │
│ (build-std compiles core + compiler_builtins)│
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Freestanding ELF binary │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Bootloader (bootimage or custom) │
│ → QEMU or real hardware │
└──────────────────────────────────────────────┘
Every layer removes a dependency on the host operating system.
Interrupt Safety and the Red Zone
┌──────────────────────────────────────────────┐
│ Red zone enabled (default ABI) │
│ │
│ Leaf function uses 128 bytes below RSP │
│ without adjusting RSP. │
└──────────────────┬───────────────────────────┘
│
│ INTERRUPT
▼
┌──────────────────────────────────────────────┐
│ CPU pushes interrupt frame onto stack │
│ → overwrites the red zone │
│ → leaf function's data is corrupted │
└──────────────────┬───────────────────────────┘
│
│ disable-redzone: true
▼
┌──────────────────────────────────────────────┐
│ Compiler never uses red zone │
│ → interrupt frame is safe │
└──────────────────────────────────────────────┘
The red zone optimization is valid for userspace but fatal for kernels.
Summary
| Item | Value |
|---|---|
| Standard library | Disabled with #![no_std] |
| Core crate | Provides Option, Result, primitives |
| Panic handler | #[panic_handler] required |
| Entry point | #[no_mangle] extern "C" fn _start |
| Unwinding | Disabled with panic = "abort" |
| Target | x86_64-unknown-none or custom JSON |
| Red zone | Must be disabled (disable-redzone: true) |
| Build std | Required for custom targets |
Key takeaways:
no_stdremoves the runtime. The kernel must provide its own panic handler, entry point, and memory layout .- The panic handler is non-negotiable. Any
no_stdbinary without a#[panic_handler]fails to link . - Unwinding must be disabled.
panic = "abort"removes the requirement foreh_personalityand reduces binary size . - The entry point must be defined.
#![no_main]and_startreplace thecrt0→mainchain . - The red zone is fatal in kernels. Interrupts can overwrite the 128-byte region below RSP.
disable-redzone: trueis mandatory . - Custom targets require
build-std.coreandcompiler_builtinsmust be compiled from source for the bare-metal target .
Remember: A no_std kernel is not a program that runs on an operating system. It is the operating system. Everything that std provides—panic handling, entry points, memory allocation, thread local storage—must be implemented explicitly. The no_std attribute is the declaration that the kernel will provide these things itself.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!