Chapter 3: Linux Address Spaces and the Virtual Memory Layout

Updated

October 31, 2026

Chapter 3: Linux Address Spaces and the Virtual Memory Layout

While hardware translation mechanics operate in units of pages and physical frames, the Linux kernel organizes memory from an architectural perspective: dividing the 64-bit virtual address continuum into well-defined user-space segments and privileged kernel zones.

Within user space, an executing application requires code segments, global initialized data, uninitialized BSS buffers, a dynamically expanding heap, thread execution stacks, and memory-mapped file regions. In kernel space, the kernel maintains direct 1:1 physical memory mappings, dynamically allocated vmalloc pools, and fixmapped I/O windows.

This chapter details the virtual memory architecture of Linux: the process address space layout, kernel virtual memory zones, architecture-specific splits, ASLR randomization, the core data structures (mm_struct and vm_area_struct), system call primitives (mmap, brk, mremap), and a hands-on experiment inspecting live process memory maps via /proc/[pid]/maps.

The Process Address Space in Linux

On modern 64-bit architectures, the full 64-bit address space (\(16\text{ Exabytes}\)) is divided into two symmetrical halves separated by a non-canonical gap:

┌────────────────────────────────────────────────────────────────────────┐
│                        64-BIT VIRTUAL MEMORY MAP (x86-64)              │
│                                                                        │
│   0xFFFFFFFFFFFFFFFF ┌──────────────────────────────────────────────┐  │
│                      │ High Kernel Fixmaps & VSYSCALL               │  │
│   0xFFFF888000000000 │ Direct Physical Memory Map (page_offset_base)│  │
│                      │ (Maps all physical RAM 1:1 with kernel base) │  │
│   0xFFFF800000000000 ├──────────────────────────────────────────────┤  │
│                      │                                              │  │
│                      │ NON-CANONICAL "HOLE"                         │  │
│                      │ (Addresses invalid in hardware; 16 Exabytes) │  │
│                      │                                              │  │
│   0x00007FFFFFFFFFFF ├──────────────────────────────────────────────┤  │
│                      │ User Stack (grows downward)                  │  │
│                      │ Memory Mappings (mmap, shared libraries)     │  │
│                      │ Heap (brk - grows upward)                    │  │
│                      │ BSS & Data Segments                          │  │
│   0x0000000000400000 │ Text Segment (Executable Code)               │  │
│   0x0000000000000000 └──────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────┘
  1. User Space (0x0000000000000000 to 0x00007FFFFFFFFFFF): The lower \(128\text{ TiB}\). Accessible by unprivileged processes executing in Ring 3.
  2. The Non-Canonical Hole: In 48-bit addressing, addresses from 0x0000800000000000 to 0xFFFF7FFFFFFFFFFF are rejected by CPU hardware. Dereferencing an address inside this range triggers a General Protection Fault (#GP).
  3. Kernel Space (0xFFFF800000000000 to 0xFFFFFFFFFFFFFFFF): The upper \(128\text{ TiB}\). Accessible only in Ring 0 supervisor mode. Protected by hardware page table supervisor bits (U/S = 0).

User-Space Layout: Code, Data, Heap, Stack, and Mapped Regions

A process’s user-space memory is partitioned into distinct logical memory segments:

  • Text Segment (.text): Contains compiled machine code instructions. Marked read-only and executable (R-X). Multiple running instances of the same binary share the identical physical text pages.
  • Data Segment (.data): Stores globally initialized variables (e.g., int global_var = 42;). Marked read-write, non-executable (RW-).
  • BSS Segment (.bss): Stores uninitialized global and static variables (Block Started by Symbol). Does not occupy disk space in the ELF binary; initialized to zeros on first write via demand paging.
  • Heap: Contiguous memory managed by brk() and sbrk(), growing upward toward higher addresses. Used by runtime allocators (malloc) for dynamic memory.
  • Memory Mapping Segment (mmap): Placed below the stack, growing downward. Accommodates dynamically loaded shared libraries (libc.so), memory-mapped files, and large anonymous allocations.
  • User Stack: Stores local function variables, saved frame pointers, and return addresses. Grows downward from the top of user space (0x7FFFFFFFFFFF).

Kernel Virtual Address Space and Direct Mapping

The upper \(128\text{ TiB}\) kernel address space contains specialized memory areas:

  • Direct Mapping (page_offset_base): A massive virtual window (beginning at 0xffff888000000000 on x86-64) that maps the entirety of physical system RAM 1:1. A physical address \(P\) corresponds to virtual address: \[V = P + \text{PAGE\_OFFSET}\] Allows the kernel to access any physical frame without modifying page tables.
  • vmalloc Pool (VMALLOC_START to VMALLOC_END): Used to allocate virtually contiguous memory buffers that may be physically non-contiguous.
  • vmemmap: An array of struct page headers representing every physical page frame in system memory.

The mm_struct and vma_struct: Core Data Structures

In kernel space, an active process’s memory space is represented by the struct mm_struct (declared in include/linux/mm_types.h):

struct mm_struct {
    struct {
        struct maple_tree mm_mt;      /* Maple Tree organizing all VMAs */
        unsigned long mmap_base;      /* Base address of mmap area */
        unsigned long task_size;      /* Size of user address space */
        pgd_t *pgd;                   /* Pointer to Page Global Directory (CR3) */
        atomic_t mm_users;            /* Number of threads sharing this mm */
        atomic_t mm_count;            /* Reference count on struct itself */
        unsigned long total_vm;       /* Total allocated pages */
        unsigned long locked_vm;      /* Pages locked in RAM (mlock) */
        unsigned long start_code, end_code;
        unsigned long start_data, end_data;
        unsigned long start_brk, brk; /* Heap boundaries */
        unsigned long start_stack;    /* Stack base address */
    };
};

The Virtual Memory Area (struct vm_area_struct - VMA)

Every contiguous virtual memory range possessing uniform permissions is represented by a VMA:

struct vm_area_struct {
    unsigned long vm_start;         /* First virtual address (inclusive) */
    unsigned long vm_end;           /* End virtual address (exclusive) */
    struct mm_struct *vm_mm;        /* Parent mm_struct */
    pgprot_t vm_page_prot;          /* Access permissions (PROT_READ, etc.) */
    unsigned long vm_flags;         /* VM_READ, VM_WRITE, VM_EXEC, VM_SHARED */
    struct file *vm_file;           /* Backing file (NULL for anonymous memory) */
    unsigned long vm_pgoff;         /* Offset in file (in page units) */
    const struct vm_operations_struct *vm_ops; /* VMA callback handlers */
};

VMAs are indexed via a Maple Tree (mm_mt), a concurrent B-tree variant optimized for storing overlapping range intervals, allowing \(O(\log N)\) lookup during page fault resolution.

Memory Region Management with mmap, brk, and mremap

User-space processes modify their virtual address space through three primary system calls:

  1. brk(void *addr): Moves the top of the process heap (the program break). Increasing brk expands heap virtual memory; decreasing it frees memory back to the kernel.
  2. mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset): Creates a new VMA.
    • Anonymous Mapping (MAP_ANONYMOUS): Allocates RAM backed by zero-filled pages rather than a file; standard mechanism for large heap allocations.
    • File-Backed Mapping: Maps a disk file directly into the process address space; reads and writes translate into page cache operations without explicit read() or write() syscalls.
  3. mremap(void *old_address, size_t old_size, size_t new_size, int flags): Expands or shrinks an existing VMA, relocating it to a new virtual address if adjacent virtual space is occupied.

Practical Experiment: Inspecting Your Process’s Memory Map

Every running process exposes its complete VMA table through the procfs pseudo-filesystem:

# Launch a background shell and inspect its memory map
cat /proc/$$/maps | head -n 15
00400000-00452000 r-xp 00000000 08:01 1234567    /usr/bin/bash        # Text
00651000-00652000 r--p 00051000 08:01 1234567    /usr/bin/bash        # Read-only Data
00652000-0065b000 rw-p 00052000 08:01 1234567    /usr/bin/bash        # Data / BSS
01a2f000-01c4e000 rw-p 00000000 00:00 0          [heap]               # Heap (brk)
7f8a1000-7f8b0000 r-xp 00000000 08:01 9876543    /lib64/libc.so.6     # Shared Lib
7ffd8a40-7ffd8a61 rw-p 00000000 00:00 0          [stack]              # User Stack
7ffd8a78-7ffd8a7c r-xp 00000000 00:00 0          [vdso]               # Kernel vDSO

Each row represents an active vm_area_struct: - 00400000-00452000: Virtual address range (vm_start - vm_end). - r-xp: Permissions (read, write, execute, private / copy-on-write). - 00000000: File offset. - 08:01 1234567: Storage device major:minor and inode number. - [heap] / [stack]: Descriptive label.