Low-Level Systems Programming

Alignment Requirements

Every type T in Rust has an alignment requirement (align_of::<T>()), measured in bytes, which specifies that the memory address of any valid instance of T must be an exact multiple of align_of::<T>(). Every alignment value is guaranteed to be a power of two ($1, 2, 4, 8, 16, \dots$), and the size of any type size_of::<T>() is always an exact multiple of its alignment.

Memory Addresses:   0x1000  0x1001  0x1002  0x1003  0x1004  0x1005  0x1006  0x1007
                    |       |       |       |       |       |       |       |
`u8`  (align = 1):  Valid   Valid   Valid   Valid   Valid   Valid   Valid   Valid
`u16` (align = 2):  Valid   --      Valid   --      Valid   --      Valid   --
`u32` (align = 4):  Valid   --      --      --      Valid   --      --      --
`u64` (align = 8):  Valid   --      --      --      --      --      --      --

Hardware and Compiler Consequences of Misalignment

Why does alignment matter?

  1. CPU Hardware Faults & Penalties: On strict architectures (such as ARMv6-M Cortex-M0 or SPARC) and on x86_64 SIMD instructions (movdqa), accessing a misaligned address triggers an immediate hardware alignment fault. Even when supported in hardware, a load that straddles two CPU cache lines requires two cache accesses.
  2. Rust Reference Invariant (Immediate UB!): In Rust's abstract machine, creating a reference (&T or &mut T) or calling ptr::read(ptr) on a misaligned pointer is unconditional Undefined Behavior, even if you never read or write through the reference! The compiler uses known alignment guarantees to optimize loops with SIMD instructions and pack niche tags into low pointer bits.

Safely Reading Misaligned Data (read_unaligned and align_to)

When parsing binary network packets or file buffers (&[u8] with alignment 1), casting a byte pointer bytes.as_ptr().cast::<u32>() and dereferencing it directly is Undefined Behavior if bytes is not 4-byte aligned.

Instead, use:

  • u32::from_le_bytes / from_be_bytes: Safe, endian-explicit conversion from [u8; 4].
  • std::ptr::read_unaligned(ptr) / write_unaligned(ptr, val): Reads or writes a value at any byte address without requiring alignment.
  • slice.align_to::<U>(): Splits a slice &[T] into an unaligned prefix &[T], a properly aligned middle slice &[U], and an unaligned suffix &[T].
use std::mem::{align_of, size_of};
use std::ptr;

#[repr(C, align(16))]
#[derive(Debug, Clone, Copy)]
pub struct SimdBlock([f32; 4]);

#[repr(C, packed)]
pub struct WireHeader {
    pub tag: u8,
    pub length: u32, // Located at offset 1 (MISALIGNED for u32!)
}

fn main() {
    println!(
        "SimdBlock: size={}B, align={}B",
        size_of::<SimdBlock>(),
        align_of::<SimdBlock>()
    );
    println!(
        "WireHeader (packed): size={}B, align={}B",
        size_of::<WireHeader>(),
        align_of::<WireHeader>()
    );

    let header = WireHeader {
        tag: 0xAA,
        length: 0x1234_5678,
    };

    // Taking `&header.length` on a `#[repr(packed)]` struct is rejected by rustc
    // because `length` sits at offset 1 (unaligned for `u32`).
    // Instead, use `&raw const header.length` with `ptr::read_unaligned`:
    let len_ptr: *const u32 = &raw const header.length;
    assert!(!len_ptr.is_aligned());

    // SAFETY: `len_ptr` points to 4 initialized bytes inside `header`, and
    // `read_unaligned` does not require 4-byte alignment.
    let len_val = unsafe { ptr::read_unaligned(len_ptr) };
    println!("Safely read unaligned packed field: {len_val:#x}");
}
SimdBlock: size=16B, align=16B
WireHeader (packed): size=5B, align=1B
Safely read unaligned packed field: 0x12345678

Checking Pointer Alignment at Runtime (ptr.is_aligned())

Stabilized in Rust 1.79, ptr.is_aligned() returns true if ptr's address is a multiple of align_of::<T>(), making runtime alignment assertions before unsafe pointer casts clean and readable.