One address space, many regions
A running process sees a large flat array of addresses — typically 48 bits on x86-64, giving 256 terabytes of virtual address space. But not all of that space is used the same way. The OS and linker carve it into regions, each with a specific purpose and protection flags.
You've been writing into these regions all along. Every global variable,
every stack frame, every malloc call — each lands in a specific region.
Understanding where things live explains why some memory errors crash immediately
while others corrupt silently, and why some addresses are valid and others are not.
The text segment
The text segment (also called the code segment) contains the compiled machine instructions of your program. It's loaded from the executable at startup and is typically read-only and executable. Read-only because the program's code shouldn't change at runtime; executable because the CPU needs to fetch instructions from it.
Writing to the text segment causes a segfault. This is intentional — it prevents both accidental corruption and certain classes of code injection attacks.
The data and BSS segments
Global and static variables live in one of two segments depending on whether they have an initializer:
- Data segment — initialized globals and statics.
int x = 42;at file scope. The initial value is stored in the executable and copied in at load time. - BSS segment — uninitialized globals and statics.
int y;at file scope. The executable records only the size; the OS zero-fills this region at load time. This is why global variables are guaranteed to be zero-initialized in C.
The heap
The heap is where dynamically allocated memory lives — everything you get from
malloc, calloc, or realloc. It starts just
above the BSS segment and grows upward toward higher addresses as you allocate more.
The heap is managed by the C runtime library (libc), which itself
uses the brk or mmap syscalls to request pages from
the OS. malloc is not a syscall — it's a user-space library function
that maintains its own free lists and only goes to the OS when it needs more pages.
free. Forget to free it:
memory leak. Free it and keep a pointer: dangling pointer. Free it twice:
undefined behavior that can corrupt the heap's internal bookkeeping.
The stack
The stack holds function call frames — local variables, function parameters, saved registers, and the return address. It starts at a high address and grows downward toward lower addresses with each function call. When the function returns, the frame is popped and that memory is available for the next call.
The stack is the fastest memory: no allocation calls, no bookkeeping, just a
decrement of the stack pointer. But it has a fixed maximum size (typically 8 MB
on Linux). Exceed it with deep recursion or large local arrays and you get a
stack overflow — your stack collides with an unmapped guard page and the OS
sends SIGSEGV.
Memory-mapped region and kernel space
Above the stack, the OS maps additional regions: shared libraries (libc, for example)
are memory-mapped into your address space so all processes can share the same
physical pages for read-only code. The mmap syscall lets your program
also map files or anonymous regions here directly.
The top of the virtual address space is reserved for the kernel. On x86-64 Linux, the upper 128 TB is kernel space — your process cannot access it directly. Attempting to read or write a kernel address causes an immediate segfault. Kernel code runs at a different privilege level; the only way to cross that boundary is through a syscall.
Inspecting your own process map
On Linux, /proc/self/maps shows your process's current memory map
at runtime. Each line shows an address range, permissions, and what it's mapped to:
Every variable in your program lives in a named region — text, data, BSS, heap, or stack — and each region has rules about lifetime, growth direction, and who manages it.