C Memory Layout: Stack, Heap, Data & Text Segments Explained
Table of Contents
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.