Your Program's View of Memory

Type this right now

Open a terminal and run:

$ cat /proc/self/maps

You just asked the cat process to show you its own memory map. You'll see something like this:

55a3b2c00000-55a3b2c02000 r--p 00000000 08:01 1234567  /usr/bin/cat
55a3b2c02000-55a3b2c07000 r-xp 00002000 08:01 1234567  /usr/bin/cat
55a3b2c07000-55a3b2c0a000 r--p 00007000 08:01 1234567  /usr/bin/cat
55a3b2c0a000-55a3b2c0b000 r--p 00009000 08:01 1234567  /usr/bin/cat
55a3b2c0b000-55a3b2c0c000 rw-p 0000a000 08:01 1234567  /usr/bin/cat
55a3b3e00000-55a3b3e21000 rw-p 00000000 00:00 0        [heap]
7f8a12000000-7f8a12200000 r--p 00000000 08:01 2345678  /usr/lib/libc.so.6
...
7ffd4a100000-7ffd4a122000 rw-p 00000000 00:00 0        [stack]

That is a real, live view of a process's memory. Every process on your system has one. Including the ones you write.


The flat address space illusion

Your program believes it has a simple, flat stretch of memory from address 0 all the way up. On a 64-bit system, addresses are 64 bits wide, but only 48 bits are actually used:

0x0000_0000_0000_0000   ← Bottom of address space
        |
        |   User space (your program lives here)
        |
0x0000_7FFF_FFFF_FFFF   ← Top of user space
        |
        |   (non-canonical gap — addresses here cause a fault)
        |
0xFFFF_8000_0000_0000   ← Bottom of kernel space
        |
        |   Kernel space (off-limits to your code)
        |
0xFFFF_FFFF_FFFF_FFFF   ← Top of address space

Your program gets the lower half. The kernel takes the upper half. Try to touch kernel space and the CPU itself — not the OS, the hardware — blocks you.

🧠 What do you think happens?

If you write *(int *)0xFFFF800000000000 = 42; in a C program, what happens? Does the compiler stop you? Does the OS stop you? Does the CPU stop you? Try it and observe the error message.


See the layout with your own code — C

This program prints the address of something from each major memory region:

// save as memview.c — compile: gcc -o memview memview.c
#include <stdio.h>
#include <stdlib.h>

int global_var = 42;            // .data section

int main() {
    int stack_var = 7;          // stack
    int *heap_var = malloc(64); // heap

    printf("Code  (main):  %p\n", (void *)main);
    printf("Global:        %p\n", (void *)&global_var);
    printf("Heap:          %p\n", (void *)heap_var);
    printf("Stack:         %p\n", (void *)&stack_var);

    free(heap_var);
    return 0;
}

Run it:

$ gcc -o memview memview.c && ./memview
Code  (main):  0x55a3b2c02149
Global:        0x55a3b2c04010
Heap:          0x55a3b3e002a0
Stack:         0x7ffd4a121a5c

Look at the addresses. Really look at them.

  • Code (0x55a...): relatively low
  • Global (0x55a...): right next to code
  • Heap (0x55a3b3...): higher, but still in the 0x55... range
  • Stack (0x7ffd...): way up high, near the top of user space

The layout reveals itself through raw addresses.


Same thing in Rust

// save as memview.rs — compile: rustc -o memview_rs memview.rs
use std::boxed::Box;

static GLOBAL_VAR: i32 = 42;

fn main() {
    let stack_var: i32 = 7;
    let heap_var = Box::new(64);

    println!("Code  (main):  {:p}", main as fn() as *const ());
    println!("Global:        {:p}", &GLOBAL_VAR as *const i32);
    println!("Heap:          {:p}", &*heap_var as *const i32);
    println!("Stack:         {:p}", &stack_var as *const i32);
}
$ rustc -o memview_rs memview.rs && ./memview_rs
Code  (main):  0x55f8a1005b30
Global:        0x55f8a1009000
Heap:          0x55f8a1a2b9d0
Stack:         0x7ffc3b2e1d44

Same pattern. Same regions. Same operating system underneath. Rust's ownership model is a compile-time concept — at runtime, it's the same address space as C.


Parsing /proc/self/maps

Every line in /proc/self/maps has this format:

address          perms offset  dev   inode  pathname
55a3b2c02000-... r-xp  00002000 08:01 12345 /usr/bin/cat
ColumnMeaning
addressVirtual address range (start-end)
permsr = read, w = write, x = execute, p = private, s = shared
offsetOffset into the file (for file-backed mappings)
devDevice (major:minor)
inodeInode number of the file
pathnameFile path, or [heap], [stack], [vdso], etc.

The permissions tell you what each region is:

  • r-xp → code (read + execute, no write)
  • rw-p → data, heap, stack (read + write, no execute)
  • r--p → read-only data (constants, string literals)

💡 Fun Fact

The [vdso] entry stands for "virtual dynamic shared object." It's a tiny piece of kernel code mapped into every process's address space so that certain system calls (like gettimeofday) can run without actually entering the kernel. It's a performance trick — the kernel pretends to be a shared library.


The big picture

High addresses
0x7FFF_FFFF_FFFF ┌─────────────────────────┐
                 │        Stack             │  ← grows downward
                 │   (local vars, frames)   │
                 ├─────────────────────────┤
                 │                         │
                 │    (unmapped gap)        │
                 │                         │
                 ├─────────────────────────┤
                 │        Heap              │  ← grows upward
                 │   (malloc, Box::new)     │
                 ├─────────────────────────┤
                 │     BSS (zeroed globals) │
                 ├─────────────────────────┤
                 │     Data (init globals)  │
                 ├─────────────────────────┤
                 │     Text (your code)     │
0x0000_0000_0000 └─────────────────────────┘
Low addresses

Every program you've ever run has this shape. The next chapter dissects each region in detail.


🔧 Task

Write a C program and a Rust program that each print the address of:

  1. A function (code region)
  2. A global/static variable (data region)
  3. A local variable (stack)
  4. A heap-allocated value

Run both. Compare the addresses. Do they fall in the same general ranges? Now run cat /proc/<PID>/maps for each (use getpid() in C or std::process::id() in Rust to print the PID, then sleep so you can inspect the maps file before the process exits).

Bonus: Pipe the output of /proc/self/maps through your own program. In C:

FILE *f = fopen("/proc/self/maps", "r");
char line[256];
while (fgets(line, sizeof(line), f)) printf("%s", line);
fclose(f);

In Rust:

#![allow(unused)]
fn main() {
println!("{}", std::fs::read_to_string("/proc/self/maps").unwrap());
}