Low-Level Systems Programming

Memory-Mapped IO Concepts

Memory-mapped I/O refers to two distinct low-level mechanisms that expose non-RAM resources through standard virtual or physical memory addresses:

  1. Hardware Memory-Mapped I/O (MMIO): Embedded peripheral registers (UARTs, timers, GPIO controllers, PCIe BARs) wired into the CPU's physical address space, accessed via read_volatile and write_volatile.
  2. OS File Memory Mapping (mmap / CreateFileMapping): Mapping a file or anonymous shared memory region directly into a process's virtual address space through the CPU's Memory Management Unit (MMU) and page tables.
1. Hardware MMIO (Physical Bus Routing):
   CPU Core ---[ Load/Store 0x4000_1000 ]---> System Bus ---> UART TX Register
   (Every read/write has hardware side effects; requires `read_volatile` / `write_volatile`)

2. OS File `mmap` (Virtual Page Table Mapping):
   Process Virtual Address (0x7f80_0000)
          |
          v  [ MMU Page Table Lookup ]
   OS Page Cache Physical Frame <=== Demand-Paged from Disk === [ File on Disk ]

Comparing Hardware MMIO and OS File mmap

Aspect Hardware MMIO (Peripheral Registers) OS File Memory Mapping (mmap)
Environment Bare-metal embedded, OS kernels, device drivers Hosted operating systems (Linux, macOS, Windows)
Backing Resource Hardware control/status registers on the system bus Files on disk or shared inter-process memory pages
Access Primitives core::ptr::read_volatile / write_volatile Byte slices or volatile/atomic pointers depending on concurrent mutation
Why &[u8] Is Dangerous Compiler may elide, reorder, or merge normal loads/stores to &T! If an external process truncates or mutates the mapped file while Rust holds &[u8], it violates Rust's aliasing/immutability rules (UB)!

Safe Hardware MMIO Register Abstraction

In bare-metal and driver code, peripheral register blocks are represented as #[repr(C)] structs accessed exclusively through raw pointers and volatile operations—never by dereferencing &mut RegisterBlock, because simply creating a Rust reference &T permits the compiler to issue speculative reads!

use std::ptr;

#[repr(C)]
pub struct UartRegisters {
    pub data: u32,   // Offset 0x00: TX/RX data register
    pub status: u32, // Offset 0x04: Bit 0 = TX Ready, Bit 1 = RX Ready
    pub ctrl: u32,   // Offset 0x08: Control register
}

pub struct UartMmio {
    base: *mut UartRegisters,
}

impl UartMmio {
    /// Wraps a base MMIO address.
    ///
    /// # Safety
    ///
    /// `base` must point to a valid, aligned `UartRegisters` MMIO region (or simulated
    /// backing struct) that is exclusively owned by this `UartMmio` instance.
    pub const unsafe fn new(base: *mut UartRegisters) -> Self {
        Self { base }
    }

    pub fn read_status(&self) -> u32 {
        // Use `&raw const` (pointer field projection) so no intermediate `&u32` reference is created!
        // SAFETY: `self.base` is valid and aligned; volatile read preserves hardware side effects.
        unsafe { ptr::read_volatile(&raw const (*self.base).status) }
    }

    pub fn write_data(&mut self, byte: u8) {
        // SAFETY: `self.base` is valid and aligned; volatile write emits exact store instruction.
        unsafe {
            ptr::write_volatile(&raw mut (*self.base).data, u32::from(byte));
        }
    }
}

fn main() {
    // Simulate a hardware register bank in aligned memory for testing
    let mut simulated_hw = UartRegisters {
        data: 0,
        status: 0b0001, // TX Ready bit set
        ctrl: 1,
    };

    let mut uart = unsafe { UartMmio::new(&raw mut simulated_hw) };
    if (uart.read_status() & 1) != 0 {
        uart.write_data(b'A');
    }

    println!("UART MMIO data register after volatile write: {:#x}", simulated_hw.data);
}
UART MMIO data register after volatile write: 0x41

Why OS File mmap Functions Are unsafe

In ecosystem crates like memmap2, Mmap::map(&file) is marked unsafe fn because the operating system does not prevent another process (or another thread in the same process) from modifying or truncating the underlying file on disk while the memory map is active:

  • If another process modifies a mapped page while Rust holds a shared &[u8] slice over that region, Rust's guarantee that shared references &T never change mid-lifetime is violated (causing Undefined Behavior).
  • If another process truncates the mapped file and Rust reads past the new end of the file, the OS kernel raises a fatal SIGBUS signal.