CMemory Layout of a C Program

Memory Layout of a C Program

Every running C program lives inside a chunk of memory that the operating system hands it, and that memory is not one undifferentiated blob — it is organized into distinct regions, each with its own purpose, lifetime, and rules. Understanding this layout is the foundation for understanding pointers, dynamic memory allocation, and a whole category of bugs (stack overflows, segmentation faults, dangling pointers) that make no sense until you know what memory region a variable actually lives in.

A compiled C program's address space is typically divided into five segments. From lowest address to highest, they are usually laid out roughly like this:

Segment

Contents

Fixed or Variable Size?

Text (Code) Segment

The compiled machine instructions of your program

Fixed at compile time; typically read-only

Initialized Data Segment

Global and static variables that have an explicit initial value

Fixed size, determined at compile time

Uninitialized Data Segment (BSS)

Global and static variables with no explicit initializer (zero-initialized)

Fixed size, determined at compile time

Heap

Memory obtained dynamically at runtime via malloc/calloc/realloc

Grows upward as you allocate; shrinks as you free

Stack

Local variables, function parameters, return addresses

Grows and shrinks automatically as functions are called and return

The Text (Code) Segment
This holds the actual machine instructions produced by the compiler — the translated form of your main() function and every other function in your program. It is typically marked read-only by the operating system, so that a stray pointer bug that tries to write into it causes an immediate crash (a segmentation fault) rather than silently corrupting your own program's instructions.
Initialized Data Segment
Global variables and static variables that you give an explicit initial value live here. Because the initial value is known at compile time, the compiler can bake it directly into the executable file.

C
int global_counter = 10;      // lives in the initialized data segment
static double pi_approx = 3.14; // also here -- static + explicit initializer
Uninitialized Data Segment (BSS)
"BSS" is a historical name (Block Started by Symbol) for the segment holding global and static variables that were not given an explicit initial value. The C standard guarantees these start out zeroed, so the compiler does not need to store any actual initial data for them in the executable file — it just reserves the right amount of zeroed space when the program starts, which keeps the binary smaller.

C
int global_total;         // no initializer -- BSS, guaranteed to start at 0
static char buffer[1024];  // no initializer -- BSS, guaranteed to start all-zero
Local variables are different
A local variable declared inside a function without an initializer is not guaranteed to be zero — it lives on the stack (see below) and starts out containing whatever garbage bytes were already there. Only uninitialized globals and statics get the automatic zero-fill guarantee.
The Heap
The heap is a pool of memory that your program requests explicitly, at runtime, using functions like malloc(), calloc(), and realloc() (all covered in depth on the following pages). Conceptually, the heap starts near the end of the data/BSS segments and grows upward, toward higher addresses, as your program allocates more memory. Heap memory is not automatically reclaimed — it stays allocated until you call free() on it (or the program exits).
The Stack
The stack holds local variables, function parameters, and bookkeeping information (such as the return address) for every function call currently in progress. It conceptually sits at the opposite end of the address space and grows downward, toward lower addresses, as functions call other functions. When a function returns, its entire chunk of stack space (its stack frame) is popped off automatically — there is nothing to free manually.

C
#include <stdio.h>

void inner(void) {
    int x = 42; // x lives on the stack, inside inner()'s stack frame
    printf("%d\n", x);
} // inner()'s stack frame (including x) is popped here, automatically

void outer(void) {
    int y = 10; // y lives on the stack, inside outer()'s stack frame
    inner();    // pushes a new stack frame on top of outer()'s
    printf("%d\n", y); // still valid -- outer()'s frame is untouched
}

int main(void) {
    outer();
    return 0;
}
Because the heap grows toward higher addresses and the stack grows toward lower addresses (conceptually facing each other across a large region of unused address space), a program that recurses too deeply or allocates a huge array of locals can exhaust the stack and crash with a stack overflow — a completely different failure mode from running out of heap memory.
Where a Variable Lives, at a Glance

Declaration

Segment

int g = 5; at file scope

Initialized data segment

int g; at file scope

BSS (uninitialized data)

static int s = 1; inside a function

Initialized data segment

static int s; inside a function

BSS (uninitialized data)

int local; inside a function

Stack

malloc(n) result

Heap

String literal "hello"

Text/read-only data segment

  • A C program's memory is divided into segments: text (code), initialized data, BSS (uninitialized data), heap, and stack

  • Global/static variables with an explicit initializer go in the initialized data segment; without one, they go in BSS and are guaranteed to start at zero

  • Local variables live on the stack and are automatically reclaimed when their function returns -- they are not zero-initialized by default

  • Memory obtained with malloc/calloc/realloc lives on the heap and stays allocated until you explicitly free it

  • The heap and stack conceptually grow toward each other; exhausting either causes a crash, but for different reasons