The Process Memory Layout
A process’s virtual address space is divided into regions with different lifetimes and permissions — text, data, BSS, heap, mapped libraries, stack — and knowing which region a variable lives in tells you how it is allocated, how long it lives and how it can be corrupted.
The problem
int, a static array, a malloced buffer and the machine code of main are all "in memory". Yet one is freed when the function returns, one exists before main runs, one you must free yourself, and one you cannot write to at all. The address space has structure; what is it?The classic picture
Reading from the low addresses up: the text segment holds machine code, mapped from the executable. Read-only data holds string literals and constants. Data holds initialised globals and statics (int limit = 100;), copied from the file at load. BSS holds zero-initialised globals and statics (static char buf[65536];); it occupies no bytes in the file — the loader just maps zero pages. Above that the heap begins and grows upward on demand. Far above, near the top of the user-space range, the stack starts and grows downward as functions are called. The gap in between is free virtual space, mostly used for memory-mapped files and shared libraries.
The two growing regions face each other on purpose: heap up, stack down, so neither needs a fixed size chosen at link time. On a 64-bit system the gap is enormous (Linux x86-64 gives user space 128 TiB with 4-level page tables), so "the heap ran into the stack" is not a real failure mode any more; on 32-bit systems with 2–3 GB it was.
- Stackgrows down · locals, return addresses, saved registers · rw-↓
- (free / mapped region)shared libraries, mmap’d files, large allocations, thread stacks↓
- Heapgrows up · malloc/new/objects · rw-↓
- BSSzero-initialised globals · rw- · zero bytes in the file↓
- Datainitialised globals · rw- · copied from the file↓
- Read-only datastring literals, constants · r--↓
- Textmachine code · r-x · shared between processes running the same binary
Permissions are the point
Each region is mapped with page-level permissions and the MMU enforces them on every access. Text is readable and executable but not writable, so a stray write into code faults immediately instead of silently patching the program. Data, BSS, heap and stack are readable and writable but not executable (W^X), so a buffer overflow that lands attacker bytes on the stack cannot simply jump to them. Read-only data is exactly that: writing to a string literal in C is undefined behaviour precisely because it is mapped r-- and the write raises SIGSEGV.
The stack additionally has a guard page below it — an unmapped page that turns runaway recursion into a clean segmentation fault rather than silent corruption of whatever was mapped below (see Stack Overflow). Thread stacks get their own guard pages. The heap has no such guard; overrunning a heap buffer corrupts the neighbouring allocation and the symptom appears somewhere else, later, which is why heap corruption is the hardest class of memory bug to localise.
- Text
r-x, rodatar--, data/BSSrw-, heaprw-, stackrw-(plus a guard page). - A write to a
r--orr-xpage →SIGSEGV(Linux/macOS) / access violation (Windows). - JIT compilers are the exception: V8 and the JVM map pages
rw-to write code, then flip them tor-xto run it.
Why your process does not look like the picture
The picture is a model. A real process differs in several ways, and most of them are deliberate. ASLR randomises the base of the executable (if it is position-independent), the libraries, the heap and the stack on every launch so that an attacker cannot predict addresses. Shared libraries are mapped in the gap, each with its own text/data/BSS, so there are many small "layouts" inside the one process. `mmap` regions appear anywhere in the gap: large mallocs (above ~128 kB in glibc) become private anonymous mappings rather than heap growth, and memory-mapped files sit there too. Every thread gets its own stack, allocated with mmap in the gap, not below the main stack.
Language runtimes add their own structure on top. A JavaScript engine or a JVM reserves a large region and runs its own allocator and garbage collector inside it; from the kernel’s point of view that is one big anonymous mapping, and "the heap" the runtime reports is not the C heap. The kernel also maps a small page of its own — the vDSO — into every process so that gettimeofday and clock_gettime need no system call.
The practical rule: use the classic picture to reason about lifetime and permissions; use /proc/<pid>/maps (Linux), vmmap (macOS) or VMMap (Windows) to see what is actually there.
Reading `/proc/<pid>/maps`
Linux exposes every mapping of a process as one line: address range, permissions, offset into the backing file, device and inode, and the path (or a bracketed name for anonymous regions). Below is a trimmed excerpt for a small dynamically linked server. Note the separate r--p, r-xp, r--p, rw-p mappings for the executable and again for libc — the linker splits each object into segments by permission — and the p (private, copy-on-write) versus s (shared) flag.
pmap -x <pid>prints the same information with RSS per mapping;smapsadds PSS and dirty-page counts.- Sum the
rw-panonymous mappings to see what the process actually allocated; the file-backedr-xppages are shared and reclaimable.
address perms offset dev inode pathname 5636a2c00000-5636a2c01000 r--p 00000000 fd:01 1315423 /opt/app/server ← ELF headers, rodata 5636a2c01000-5636a2c05000 r-xp 00001000 fd:01 1315423 /opt/app/server ← text 5636a2c05000-5636a2c06000 r--p 00005000 fd:01 1315423 /opt/app/server ← more rodata 5636a2c06000-5636a2c07000 rw-p 00006000 fd:01 1315423 /opt/app/server ← data (+ bss follows) 5636a4a1e000-5636a4a3f000 rw-p 00000000 00:00 0 [heap] 7f2d1c000000-7f2d1c021000 rw-p 00000000 00:00 0 ← anonymous (mmap'd malloc) 7f2d1c200000-7f2d1c228000 r--p 00000000 fd:01 3932 /usr/lib/x86_64-linux-gnu/libc.so.6 7f2d1c228000-7f2d1c3bd000 r-xp 00028000 fd:01 3932 /usr/lib/x86_64-linux-gnu/libc.so.6 7f2d1c3bd000-7f2d1c415000 r--p 001bd000 fd:01 3932 /usr/lib/x86_64-linux-gnu/libc.so.6 7f2d1c415000-7f2d1c419000 rw-p 00214000 fd:01 3932 /usr/lib/x86_64-linux-gnu/libc.so.6 7f2d1c43a000-7f2d1c43c000 r-xp 00000000 fd:01 3928 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffd8b6e2000-7ffd8b703000 rw-p 00000000 00:00 0 [stack] 7ffd8b7c9000-7ffd8b7cb000 r-xp 00000000 00:00 0 [vdso]
Key points
- Text (code, r-x), rodata (r--), data (initialised globals, rw-), BSS (zero-initialised, rw-, no file bytes), heap (grows up), stack (grows down), with libraries and mmap regions in the gap.
- Permissions are enforced per page by the MMU; text is not writable and data is not executable, and a violation is an immediate
SIGSEGV. - A guard page under each stack turns runaway recursion into a clean crash; heap overruns corrupt neighbours silently.
- Real layouts vary: ASLR randomises bases, libraries carry their own segments, large mallocs are mmap’d, threads get separate stacks, runtimes manage their own arenas.
/proc/<pid>/mapsis the ground truth on Linux; read it before trusting the diagram.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why split a program into segments with different permissions?
So the hardware can catch whole classes of bugs and attacks for free: writes into code, execution of data, reads past the stack.
▸Why does BSS exist as a separate region?
A 64 MB zero-initialised array should not cost 64 MB of executable on disk; the loader maps zero pages that are only materialised when written.
▸Why do heap and stack grow toward each other?
Historically so that neither needed a fixed size; on 64-bit the gap is so large it no longer matters, but the shape survived.
▸Why randomise the layout?
Return-oriented and buffer-overflow exploits need to know addresses; ASLR makes each run different so a single leaked address is not enough.
Process memory layout
How it fails
What the failure looks like from inside real software.
SIGSEGVwriting to a string literal: the pointer targetsr--rodata.- A deep recursion crashes with a segmentation fault rather than an out-of-memory error: it hit the stack guard page — see Stack Overflow.
- A heap buffer overflow shows up as a crash inside
free()ormalloc()long after the overflow, because it corrupted the allocator’s bookkeeping in the neighbouring chunk. - A
std::threadprogram crashes when creating its 3,000th thread: each default thread stack reserves 8 MB of virtual space (glibc) and the process hit its address-space or map-count limit, not RAM. - A JIT runtime is blocked by a hardened kernel that refuses
rw-→r-xtransitions, and the program fails at start-up with a permissions error nobody in the application wrote.