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:
- 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_volatileandwrite_volatile. - 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&Tnever 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
SIGBUSsignal.