Skip to content

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

  1. Code Segment (Text): Contains the actual program instructions. The processor fetches instructions from the code segment and executes them.
  2. Data Segment: Stores global variables, constants, and static data. This is any string, number, or other data that is not a program instruction.
  3. Heap: Dynamically allocated memory (malloc, new, etc.), accessed via pointers and references and allocated using malloc or new in C/C++.
  4. 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:

  1. Security: Prevents buffer overflows from jumping outside the program's address space
  2. Address space utilization: Gives the program access to the full virtual address space
  3. 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:

  1. Page Tables: OS maintains mapping between virtual and physical addresses in a page table.
  2. 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.
  3. Memory Protection: Each process can only access its own pages.

Benefits of Virtual Memory

  1. Isolation: Processes cannot access each other's memory
  2. Efficiency: During memory pressure, only active pages are kept in physical RAM
  3. Flexibility: Each process gets the illusion of full address space
  4. 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 executable

Address 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

  1. Stack and heap grow in opposite directions for efficient memory utilization
  2. Virtual memory gives each process the illusion of complete address space
  3. Stack base at 0xFFFFFFFF prevents buffer overflows and ensures security
  4. Paging maps virtual addresses to physical memory locations
  5. Memory protection prevents processes from accessing each other's memory
  6. Understanding memory layout is crucial for debugging and optimization