Appearance
Process Memory Model
Video: Memory Segments in C/C++
When you run a program, the operating system assigns a virtual address space that appears to be a continuous block of memory to the running process. This article explores the process memory model, including why stack and heap grow in opposite directions, how address spaces are organized, and the role of virtualization.
Process Memory Layout
Basic Memory Segments
Every process has a virtual address space divided into several key segments:
cpp
High Address (0xFFFFFFFF)
┌─────────────────────────────────────┐
│ Stack │ ← Grows downward
│ ↓ │
├─────────────────────────────────────┤
│ ↓ │
│ ↓ │
├─────────────────────────────────────┤
│ Heap │ ← Grows upward
│ ↑ │
├─────────────────────────────────────┤
│ Data Segment │
│ (Global Variables) │
├─────────────────────────────────────┤
│ Code Segment │
│ (Program Instructions) │
└─────────────────────────────────────┘
Low Address (0x00000000)Key Memory Segments
- Code Segment (Text): Contains the actual program instructions. The processor fetches instructions from the code segment and executes them.
- Data Segment: Stores global variables, constants, and static data. This is any string, number, or other data that is not a program instruction.
- Heap: Dynamically allocated memory (malloc, new, etc.), accessed via pointers and references and allocated using malloc or new in C/C++.
- Stack: Local variables, function parameters, and return addresses.
Why Stack and Heap Grow in Opposite Directions
The Problem with Same Direction Growth
Imagine if both stack and heap grew upward:
cpp
Memory Space:
┌─────────────────────────────────────┐
│ Stack: [func1][func2][func3] ↑ │
├─────────────────────────────────────┤
│ Heap: [block1][block2][block3] ↑ │
└─────────────────────────────────────┘Issues:
- Fixed limits: You'd need to decide in advance how much space to allocate to each
- Wasteful: If stack uses little memory, heap space is wasted (and vice versa)
- Fragmentation: Memory becomes fragmented as stack and heap compete for space
The Solution: Opposite Direction Growth
cpp
Memory Space:
┌─────────────────────────────────────┐
│ Stack: [func3][func2][func1] ↓ │
├─────────────────────────────────────┤
│ Free Space │
├─────────────────────────────────────┤
│ Heap: [block1][block2][block3] ↑ │
└─────────────────────────────────────┘Benefits:
- Dynamic allocation: Both can grow as needed until they meet
- Efficient use: No wasted space - all available memory is utilized
- Flexible: Stack can be large when heap is small, and vice versa
Memory Address Organization
How Addresses are Assigned
Modern operating systems use virtual memory to give each process the illusion of having its own complete address space. On 32-bit systems, this address space is 4GB (2^32 bytes).
cpp
Physical RAM (4GB):
┌─────────────────────────────────────┐
│ Process A: 0x00000000-0xFFFFFFFF │
│ Process B: 0x00000000-0xFFFFFFFF │
│ Process C: 0x00000000-0xFFFFFFFF │
└─────────────────────────────────────┘Key Point: Each process thinks it has access to the full address space (0x00000000 to 0xFFFFFFFF on 32-bit systems), but the OS maps these virtual addresses to different physical memory locations. In reality, the physical memory is much smaller than the virtual address space.
The OS reserves some virtual addresses for its own use, so the stack starts at a very high address in the virtual address space, typically near the top of the available address range. This placement serves several important purposes:
- Security: Prevents buffer overflows from jumping outside the program's address space
- Address space utilization: Gives the program access to the full virtual address space
- Memory layout optimization: Allows both stack and heap to grow without interfering with each other
cpp
32-bit Address Space:
┌─────────────────────────────────────┐
│ 0xFFFFFFFF ← Very high address │
│ ↓ │
│ Stack │
│ (grows downward) │
├─────────────────────────────────────┤
│ Heap │
│ (grows upward) │
│ ↑ │
├─────────────────────────────────────┤
│ Data Segment │
├─────────────────────────────────────┤
│ Code Segment │
│ 0x00000000 ← Code starts here │
└─────────────────────────────────────┘Modern Systems: In 64-bit systems, the stack typically starts at addresses like 0x7fffffffffff or similar high addresses, not at the absolute maximum. The exact address depends on the operating system and memory layout policies.
Virtual Memory and Paging
The Illusion of Complete Address Space
Operating systems use virtual memory and pagingA memory management technique that divides memory into fixed-size pages and maps virtual addresses to physical memory to prevent over-allocating memory while still giving each process the illusion of having its own complete address space. The physical memory is divided into fixed-size pages, and the virtual memory is divided into fixed-size pages. The OS maintains a mapping between the virtual and physical addresses. The physical RAM may actually look something like this:
cpp
Physical RAM (4GB):
┌─────────────────────────────────────┐
│ Page Frame 0: Process A Stack │
│ Page Frame 1: Process B Heap │
│ Page Frame 2: Process A Code │
│ Page Frame 3: Process C Data │
│ ... │
└─────────────────────────────────────┘How it works:
- Page Tables: OS maintains mapping between virtual and physical addresses in a page table.
- Page Faults: When process accesses unmapped memory, the OS assigns a new page from the disk, stores it in the TLB, and gives a pointer to the process.
- Memory Protection: Each process can only access its own pages.
Benefits of Virtual Memory
- Isolation: Processes cannot access each other's memory
- Efficiency: During memory pressure, only active pages are kept in physical RAM
- Flexibility: Each process gets the illusion of full address space
- Security: Memory protection prevents unauthorized access
Memory Growth Patterns
Stack Growth
The stack grows downward (toward lower addresses) as functions are called:
cpp
void func1() {
int local1 = 10; // Stack: [local1]
func2(); // Stack: [local1][func2_frame]
}
void func2() {
int local2 = 20; // Stack: [local1][func2_frame][local2]
func3(); // Stack: [local1][func2_frame][local2][func3_frame]
}
void func3() {
int local3 = 30; // Stack: [local1][func2_frame][local2][func3_frame][local3]
// When func3 returns: Stack: [local1][func2_frame][local2]
// When func2 returns: Stack: [local1]
// When func1 returns: Stack: }Stack Frame Contents:
- Function parameters
- Local variables
- Return address
- Previous frame pointer
Heap Growth
The heap grows upward (toward higher addresses) as memory is allocated:
cpp
int* ptr1 = new int(10); // Heap: [block1]
int* ptr2 = new int(20); // Heap: [block1][block2]
int* ptr3 = new int(30); // Heap: [block1][block2][block3]
delete ptr2; // Heap: [block1][hole][block3]
int* ptr4 = new int(20); // Heap: [block1][block4][block3]Heap Management:
- Allocation: System finds free space and marks it as used
- Deallocation: System marks space as free (may not immediately return to OS)
- Fragmentation: Over time, heap may become fragmented with small free spaces
Practical Examples
Memory Layout in C++
cpp
#include <iostream>
#include <cstdint>
// Global variables (Data Segment)
int global_var = 42;
const int global_const = 100;
int main() {
// Local variables (Stack)
int local_var = 10;
int* heap_ptr = new int(20); // Heap allocation
std::cout << "Global variable address: " << &global_var << std::endl;
std::cout << "Local variable address: " << &local_var << std::endl;
std::cout << "Heap allocation address: " << heap_ptr << std::endl;
// Notice: Stack addresses are typically higher than heap addresses
// (because stack grows downward from high addresses)
delete heap_ptr; // Free heap memory
return 0;
}Advanced Topics
Memory Mapping
Modern systems use memory mapping for files and shared libraries:
cpp
#include <sys/mman.h>
// Memory map a file
int fd = open("data.bin", O_RDONLY);
void* mapped_data = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);Memory Protection
Different memory regions have different permissions:
cpp
// Code segment: Read-only, executable
// Data segment: Read-write, not executable
// Stack: Read-write, not executable
// Heap: Read-write, not executableAddress Space Layout Randomization (ASLR)
Modern systems randomize memory layout for security:
cpp
Traditional Layout: ASLR Layout:
┌─────────────────┐ ┌─────────────────┐
│ Stack: 0xFFFF │ │ Stack: 0x???? │
├─────────────────┤ ├─────────────────┤
│ Heap: 0x1000 │ │ Heap: 0x???? │
├─────────────────┤ ├─────────────────┤
│ Code: 0x0000 │ │ Code: 0x???? │
└─────────────────┘ └─────────────────┘Key Takeaways
- Stack and heap grow in opposite directions for efficient memory utilization
- Virtual memory gives each process the illusion of complete address space
- Stack base at 0xFFFFFFFF prevents buffer overflows and ensures security
- Paging maps virtual addresses to physical memory locations
- Memory protection prevents processes from accessing each other's memory
- Understanding memory layout is crucial for debugging and optimization