C memory layout with stack heap data BSS and text segments diagram

C Memory Layout: Stack, Heap, Data & Text Segments Explained

Memory Layout Overview

When you compile and run a C program, the operating system does not just dump your code into memory randomly. It organizes everything into specific regions called segments. Understanding the C memory layout is like understanding the floor plan of a building — once you know where everything lives, debugging becomes dramatically easier.

Every C program has five main memory segments, arranged from low addresses to high addresses:

+---------------------------+  High Address (0xFFFFFFFF on 32-bit)
|         Stack             |  ↓ Grows downward
|           ↓               |
|         (gap)             |
|           ↑               |
|         Heap              |  ↑ Grows upward
+---------------------------+
|    BSS (Uninitialized)    |
+---------------------------+
|   Data (Initialized)      |
+---------------------------+
|    Text (Code)            |  Low Address (0x00000000)
+---------------------------+

The stack grows downward toward the heap. The heap grows upward toward the stack. The gap between them is virtual memory that neither has claimed yet. If they ever collide, your program is in serious trouble.

Text Segment (Code)

The text segment contains your compiled machine instructions — the actual executable code. When you write printf("Hello"), the compiler turns that into CPU instructions, and those instructions live here.

#include <stdio.h>

void greet(void) {          // Machine code for greet() → text segment
    printf("Hello!\n");     // printf call instruction → text segment
}

int main(void) {            // Machine code for main() → text segment
    greet();
    return 0;
}

Key properties:

  • Read-only — The OS marks this segment as non-writable. Trying to modify code at runtime triggers a segmentation fault. This prevents accidental (or malicious) code modification.
  • Shared — If you run two instances of the same program, they share the same text segment in physical memory. The OS is smart enough not to duplicate it.
  • Fixed size — Determined at compile time and never changes during execution.

String literals like "Hello" are also stored in a read-only section near the text segment. That is why modifying a string literal is undefined behavior:

char *s = "Hello";
s[0] = 'J';  // UNDEFINED BEHAVIOR — "Hello" is in read-only memory

Data Segment

The data segment stores initialized global and static variables — variables that have an explicit value assigned at declaration:

int global_count = 42;              // Data segment — initialized global
static float pi = 3.14159f;        // Data segment — initialized static

void example(void) {
    static int call_count = 0;     // Data segment — initialized local static
    call_count++;
}

These variables exist for the entire lifetime of the program. They are allocated before main() runs and freed after main() returns. The initial values are stored in the executable file itself — which is why executables with large initialized arrays can be surprisingly big.

Key properties:

  • Read-write — Unlike text, data segment is writable
  • Lifetime — Entire program execution
  • Scope — Depends on declaration (global vs static)

BSS Segment

BSS (Block Started by Symbol) stores uninitialized global and static variables. In C, these are automatically initialized to zero:

int uninitialized_global;           // BSS — will be 0
static double static_array[1000];   // BSS — all zeros, but no space in executable
char big_buffer[1000000];           // BSS — 1MB, but adds 0 bytes to executable

int initialized = 42;               // NOT BSS — this goes to data segment

The BSS segment is a clever optimization. Imagine you declare char buffer[1000000] as a global. Instead of storing one million zeros in your executable file, the compiler simply records “allocate 1,000,000 bytes and zero them.” The executable stays small, and the OS zeroes the memory at load time.

This is why int x; at global scope is guaranteed to be zero in C — it lives in BSS, and BSS is always zero-initialized.

Heap Segment

The heap is where dynamic memory allocation happens. Every call to malloc, calloc, or realloc grabs memory from the heap:

#include <stdlib.h>

int main(void) {
    // All of these live on the heap
    int *numbers = malloc(100 * sizeof(int));
    char *name = calloc(50, sizeof(char));
    double *matrix = malloc(10 * 10 * sizeof(double));

    // You control their lifetime
    free(numbers);
    free(name);
    free(matrix);
    return 0;
}

Key properties:

  • Grows upward — From low addresses toward the stack
  • Programmer-managed lifetime — You decide when to allocate and free
  • Fragmentation risk — Repeated malloc/free can leave gaps
  • Slower than stack — Allocation involves searching free lists, system calls
  • Survives function calls — Heap memory persists until explicitly freed

The heap is managed by a memory allocator (like glibc’s ptmalloc or jemalloc). When you call malloc, the allocator searches its free list. If no suitable block exists, it asks the OS for more pages using sbrk() or mmap().

Stack Segment

The stack is the workhorse of function execution. Every time you call a function, a stack frame is pushed containing the function’s local variables, parameters, return address, and saved registers:

#include <stdio.h>

void inner(int c) {
    int local_c = c * 2;     // Stack frame for inner()
    printf("%d\n", local_c);
}

void outer(int a, int b) {
    int sum = a + b;          // Stack frame for outer()
    inner(sum);
}

int main(void) {
    int x = 10, y = 20;      // Stack frame for main()
    outer(x, y);
    return 0;
}

When main() calls outer(), a new frame is pushed on top. When outer() calls inner(), another frame is pushed. When inner() returns, its frame is popped. The stack naturally handles function call nesting.

Stack during inner() execution:
+---------------------------+
| inner: c=30, local_c=60   |  ← Stack pointer (top)
+---------------------------+
| outer: a=10, b=20, sum=30 |
+---------------------------+
| main: x=10, y=20          |
+---------------------------+

Key properties:

  • Grows downward — From high addresses toward the heap
  • Automatic lifetime — Variables die when the function returns
  • Very fast — Allocation is just moving the stack pointer
  • Limited size — Typically 1–8 MB (configurable with ulimit -s)
  • LIFO order — Last function called, first to return

Seeing It In Action

Here is a program that shows where each type of variable lives:

#include <stdio.h>
#include <stdlib.h>

// Global variables
int initialized_global = 100;    // Data segment
int uninitialized_global;        // BSS segment

int main(void) {
    // Local variables
    int stack_var = 42;          // Stack
    static int static_var = 99;  // Data segment

    // Dynamic memory
    int *heap_var = malloc(sizeof(int));

    printf("=== Memory Addresses ===\n");
    printf("Text:  main()              = %p\n", (void *)main);
    printf("Data:  initialized_global  = %p\n", (void *)&initialized_global);
    printf("Data:  static_var          = %p\n", (void *)&static_var);
    printf("BSS:   uninitialized_global= %p\n", (void *)&uninitialized_global);
    printf("Heap:  heap_var            = %p\n", (void *)heap_var);
    printf("Stack: stack_var           = %p\n", (void *)&stack_var);

    free(heap_var);
    return 0;
}

Typical output (addresses will vary):

=== Memory Addresses ===
Text:  main()              = 0x401136
Data:  initialized_global  = 0x404030
Data:  static_var          = 0x404038
BSS:   uninitialized_global= 0x40403c
Heap:  heap_var            = 0x1a4d2a0
Stack: stack_var           = 0x7ffd5e3a4c5c

Notice the pattern: text has the lowest address, followed by data and BSS. The heap is much higher, and the stack is highest of all — with a massive gap between heap and stack.

Stack Overflow & Heap Exhaustion

Stack Overflow

The most common cause is infinite or deep recursion:

void infinite_recursion(int n) {
    int big_array[1000];  // 4KB per frame
    printf("Depth: %d\n", n);
    infinite_recursion(n + 1);  // Stack overflow!
}

Each call adds ~4KB to the stack. With a 1MB stack limit, you crash after about 250 calls. The fix? Use iteration instead of recursion, or reduce local variable sizes.

Heap Exhaustion

// Allocating without freeing
while (1) {
    int *p = malloc(1000000);  // 1MB per iteration
    if (!p) {
        printf("Heap exhausted!\n");
        break;
    }
    // Never freed — memory leak accumulates
}

Segment Summary

Segment Contains Lifetime Access Size
Text Machine code, string literals Entire program Read-only Fixed
Data Initialized globals/statics Entire program Read-write Fixed
BSS Uninitialized globals/statics Entire program Read-write Fixed
Heap malloc/calloc/realloc Until free() Read-write Dynamic ↑
Stack Local variables, parameters Until function returns Read-write Dynamic ↓

Practice Exercises

Exercise 1: Memory Map

Write a program that declares one variable in each memory segment (text, data, BSS, heap, stack). Print the address of each and verify they follow the expected layout order.

Exercise 2: Stack Depth Measurement

Write a recursive function that counts how deep it can recurse before hitting a stack overflow. Print the depth count using a static variable. Compare results with different ulimit -s settings.

Exercise 3: Executable Size Experiment

Create two programs: one with int arr[1000000] = {1}; (data segment) and another with int arr[1000000]; (BSS segment). Compare executable file sizes with ls -la and explain the difference.

Understanding C memory layout transforms you from someone who writes code to someone who understands what the machine actually does with that code. Next, we will explore what happens when things go wrong — memory bugs that haunt every C programmer.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *