Topic 95 of 100 Advanced C Programming Interactive Tutorial

C Memory Layout: Stack, Heap, Data, Code, and Memory Segments

15 mins read Last updated: 22 Aug 2026 34,500 views
C Compiler 0.00%

What is C Memory Layout? Core Concept & Direct Summary

Core Objective & Problem Statement

What is the memory segment layout of a compiled C executable: Text, Initialized Data, BSS, Heap, and Stack?

Direct Answer Summary

When an operating system executes a C program, it does not simply load machine instructions into physical RAM at random. Instead, the OS loader, memory management unit (MMU), and runtime environment map the process into a structured virtual address space.

Understanding how your variables, functions, string literals, and dynamic buffers reside in memory is the single most important milestone in transitioning from a beginner C coder to an advanced systems programmer. It explains:

  • Why modifying "hello" crashes with a Segmentation Fault while modifying char str[] = "hello" is completely safe.
  • Why global variables default to zero while uninitialized local stack variables contain garbage values.
  • How stack overflows occur during unbounded recursion.
  • How heap fragmentation impacts long-running servers and embedded systems.

Let's dissect the classic 5-segment memory architecture of a compiled C binary, trace its layout from low memory addresses to high memory addresses, and examine real memory address behavior.

How Does C Memory Layout Work? In-Depth Mechanics & Architecture

. High-Level Architecture of Process Memory

In modern 32-bit and 64-bit operating systems (Linux, Windows, macOS), every user-space process receives its own private Virtual Address Space.

Below is the standard layout of a compiled C program in virtual memory, ordered from lowest memory address (0x0000...) to highest memory address (0xFFFF...):

Architecture Diagram
High Memory (0xFFFFFFFF / 0x7FFFFFFFFFFF)┌────────────────────────────────────────────────────────┐│  Kernel Space Mapping (Inaccessible to User Space)     │├────────────────────────────────────────────────────────┤│  Command-Line Arguments (`argv`) & Environment (`envp`)│├────────────────────────────────────────────────────────┤│  STACK SEGMENT                                         ││  - Local function variables, return addresses, frames  ││  - Grows DOWNWARDS (High address ──► Low address)      ││                         │                              ││                         ▼                              ││                 [ Stack Overflow ]                     ││                                                        ││                         ▲                              ││                         │                              ││  - Grows UPWARDS (Low address ──► High address)        ││  HEAP SEGMENT                                          ││  - Dynamic allocations (`malloc`, `calloc`, `realloc`) │├────────────────────────────────────────────────────────┤│  Memory Mapping Segment (`mmap`, shared libraries)     │├────────────────────────────────────────────────────────┤│  BSS SEGMENT (Block Started by Symbol)                 ││  - Uninitialized & zero-initialized globals & statics  │├────────────────────────────────────────────────────────┤│  INITIALIZED DATA SEGMENT (.data)                      ││  - Non-zero initialized globals and statics            │├────────────────────────────────────────────────────────┤│  TEXT / CODE SEGMENT (.text & .rodata)                 ││  - Compiled machine instructions, read-only constants  │└────────────────────────────────────────────────────────┘Low Memory (0x00000000 / 0x000000000000)

2. Deep Dive Into the Five Segments

Segment 1: The Text (Code) Segment

The Text Segment (often called .text) contains the compiled machine code instructions executed by the CPU.

  • Permissions: Read-Only + Execute (r-x). Writing to the code segment triggers a hardware-enforced memory protection fault (SIGSEGV on POSIX, Access Violation on Windows).
  • Sharing: Because it is read-only, multiple running instances of the same binary (e.g., ten bash shells) can safely share the exact same physical memory pages for the text segment.
  • Read-Only Data (.rodata): Embedded constants and string literals (e.g., "Hello, World!") reside either directly in .text or in an adjacent .rodata section.
main.c
Run Online
const char *msg = "Constant String";
/* Pointer 'msg' is in Data or Stack; "Constant String" is in .rodata */void calculate(void) {
    /* Machine instructions for this function live in .text */ /* ... */
}

Segment 2: The Initialized Data Segment (.data)

The Initialized Data Segment holds global variables, file-scope static variables, and function-scope static variables that have been explicitly initialized to a non-zero value in your source code.

  • Permissions: Read-Write (rw-).
  • Binary Footprint: Because these initial values must be loaded when the program starts, they directly increase the compiled executable size on disk.
main.c
Run Online
int global_counter = 100;
/* Lives in .data */static double pi_approx = 3.1415;
/* Lives in .data */
void counter_tick(void) {
    static int calls = 1;
    /* Lives in .data (retains value across calls) */
}

Segment 3: The BSS Segment (Uninitialized Data)

BSS stands historically for "Block Started by Symbol" (an assembly pseudo-op from the 1950s). It contains:

  1. Global variables that are declared without an explicit initializer.
  2. Global or static variables explicitly initialized to 0 or NULL.
  3. File-scope and function-scope static variables that are uninitialized.
  • Permissions: Read-Write (rw-).
  • Zero-Initialization Guarantee: The ISO C standard mandates that all variables in BSS are initialized to zero (or null pointers) before main() begins execution.
  • Binary Footprint Advantage: Unlike .data, variables in .bssdo not consume space in the executable binary on disk. The executable header merely stores a numeric field recording how many bytes the BSS segment requires. When the OS loads the binary into memory, it maps a block of zeroed RAM pages for the BSS.
main.c
Run Online
int uninitialized_global;
/* Lives in .bss (automatically 0) */static int zero_flag = 0;
/* Lives in .bss (optimized to .bss instead of .data) */int large_buffer[1000000];
/* Takes only a few bytes in the .exe on disk;
allocated in .bss at runtime */

Segment 4: The Heap (Dynamic Memory)

The Heap provides run-time dynamic memory allocation managed by the programmer via malloc(), calloc(), realloc(), and free().

  • Permissions: Read-Write (rw-).
  • Growth Direction: On traditional architectures, the heap grows upwards toward higher memory addresses. The lower boundary is managed via the system brk / sbrk pointer or OS page allocation functions (mmap on Linux, VirtualAlloc on Windows).
  • Lifecycle: Persistent until explicitly deallocated by the programmer or until the process terminates. Failure to free memory causes memory leaks; using memory after freeing causes undefined behavior (use-after-free).
main.c
Run Online
int *arr =(int *)malloc(100 * sizeof(int));
/* Memory allocated on Heap; 'arr' pointer resides on Stack */free(arr);
/* Returned to the allocator's free list */

Segment 5: The Stack Segment

The Stack manages function call execution frames (activation records). It allocates storage automatically when entering functions and reclaims it upon return.

  • Permissions: Read-Write (rw-).
  • Growth Direction: On modern x86, x86-64, and ARM architectures, the call stack grows downwards from high memory addresses toward lower memory addresses.
  • Contents of a Stack Frame:
    1. Return address (where the instruction pointer should jump when the function returns).
    2. Saved frame pointer (%rbp / %ebp).
    3. Function arguments passed via stack (when registers are exhausted).
    4. Non-static local variables declared within function blocks.
    5. Saved CPU registers.
main.c
Run Online
void compute(int a) {
    int local_result = a * 2;
    /* Allocated within compute()'s stack frame */
}
/* Automatically popped and destroyed at closing brace */

3. Verified Memory Segment Inspection Code

The following complete, standalone C program prints the virtual addresses of variables and functions located in every memory segment, demonstrating their spatial separation:

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
/*
1. Initialized Data Segment (.data) */int global_initialized = 42;
const char *global_str_literal = "I live in .rodata / text";
/*
2. Uninitialized Data Segment (.bss) */int global_uninitialized;
int global_zero_initialized = 0;
static int static_uninitialized;
/*
3. Text (Code) Segment */void sample_function(void) {
    /* Dummy function */
}
void inspect_stack_growth(int recursion_depth) {
    int local_frame_var = recursion_depth;
    printf("   Stack frame depth %d address : %p\n", recursion_depth,(void *)&local_frame_var);
    if (recursion_depth < 3) {
        inspect_stack_growth(recursion_depth + 1);
    }
}
int main(int argc, char *argv[]) {
    printf("============================================================\n");
    printf("       C PROCESS MEMORY SEGMENT ADDRESS INSPECTION          \n");
    printf("============================================================\n\n");
    /* Text Segment */ printf("1. TEXT / CODE SEGMENT:\n");
    printf("   main() function address      : %p\n",(void *)(uintptr_t)main);
    printf("   sample_function() address    : %p\n",(void *)(uintptr_t)sample_function);
    printf("   String literal address       : %p\n\n",(void *)global_str_literal);
    /* Initialized Data Segment */ printf("2. INITIALIZED DATA SEGMENT (.data):\n");
    printf("   global_initialized address   : %p\n\n",(void *)&global_initialized);
    /* BSS Segment */ printf("3. BSS SEGMENT (Zero / Uninitialized Data):\n");
    printf("   global_uninitialized address : %p\n",(void *)&global_uninitialized);
    printf("   global_zero_init address     : %p\n",(void *)&global_zero_initialized);
    printf("   static_uninit address        : %p\n\n",(void *)&static_uninitialized);
    /* Heap Segment */ printf("4. HEAP SEGMENT (Dynamic Allocation):\n");
    int *heap_alloc_1 =(int *)malloc(sizeof(int));
    int *heap_alloc_2 =(int *)malloc(sizeof(int));
    printf("   heap_alloc_1 address         : %p\n",(void *)heap_alloc_1);
    printf("   heap_alloc_2 address         : %p\n\n",(void *)heap_alloc_2);
    /* Stack Segment */ printf("5. STACK SEGMENT (Local Function Variables):\n");
    int local_stack_var = 99;
    printf("   local_stack_var address      : %p\n\n",(void *)&local_stack_var);
    /* Stack Growth Direction Demonstration */ printf("6. STACK GROWTH DIRECTION (Recursive Calls):\n");
    inspect_stack_growth(1);
    /* High Memory: Arguments & Environment */ printf("\n7. HIGH MEMORY (CLI Args & Environment):\n");
    printf("   Address of argc              : %p\n",(void *)&argc);
    printf("   Address of argv[0] pointer   : %p\n",(void *)argv[0]);
    free(heap_alloc_1);
    free(heap_alloc_2);
    return 0;
}

Typical Output Analysis (64-bit Architecture)

main.c
Run Online
============================================================ C PROCESS MEMORY SEGMENT ADDRESS INSPECTION ============================================================
1. TEXT / CODE SEGMENT: main() function address : 0x00401560 sample_function() address : 0x00401550 String literal address : 0x00407020
2. INITIALIZED DATA SEGMENT(.data): global_initialized address : 0x00408010
3. BSS SEGMENT(Zero / Uninitialized Data): global_uninitialized address : 0x00409030 global_zero_init address : 0x00409034 static_uninit address : 0x00409038
4. HEAP SEGMENT(Dynamic Allocation): heap_alloc_1 address : 0x008e1690 heap_alloc_2 address : 0x008e16b0
5. STACK SEGMENT(Local Function Variables): local_stack_var address : 0x0061ff1c
6. STACK GROWTH DIRECTION(Recursive Calls): Stack frame depth 1 address : 0x0061fedc Stack frame depth 2 address : 0x0061feac(Decreased by 48 bytes!) Stack frame depth 3 address : 0x0061fe7c(Decreased by 48 bytes!)
7. HIGH MEMORY(CLI Args & Environment): Address of argc : 0x0061ff20 Address of argv[0] pointer : 0x008e0e70

Key Observations from the Address Trace:

  1. Low to High Progression: The Text segment occupies the lowest address space (0x0040...), followed by .data, .bss, the Heap (0x008e...), and finally the Stack located far higher in memory (0x0061ff...).
  2. Stack Growth (Downward): Notice the recursion addresses:
    • Depth 1: 0x0061fedc
    • Depth 2: 0x0061feac (32 bytes lower)
    • Depth 3: 0x0061fe7c (32 bytes lower)
      Each nested function call pushes a new stack frame onto the stack, reducing the pointer address.
  3. Heap Growth (Upward):heap_alloc_2 (0x008e16b0) resides at a higher address than heap_alloc_1 (0x008e1690), reflecting upward allocation.

4. Stack vs. Heap: The Definitive Comparison

The choice between stack and heap memory allocation is one of the most critical design decisions in C programming:

PropertyStack AllocationHeap Allocation
Allocation MechanismHandled automatically by CPU via stack pointer manipulation (sub rsp, bytes).Handled by software runtime allocator (malloc, glibc ptmalloc, jemalloc).
SpeedBlazing fast (1 CPU instruction clock cycle).Slower (requires searching free lists, thread locks, OS page requests).
Size LimitStrict and small (typically 1MB–8MB default OS thread stack).Huge (limited only by available physical RAM and virtual swap space).
DeallocationAutomatic when the function or block scope exits.Manual (free() required). Easy to leak or double-free.
Variable SizingFixed compile-time size (except C99 Variable Length Arrays).Dynamic runtime re-sizing via realloc().
Failure ModeUncatchable crash: Stack Overflow (Guard page violation).Catchable: malloc() returns NULL on out-of-memory.
FragmentationZero fragmentation (strictly contiguous LIFO order).Susceptible to external and internal fragmentation over time.

5. Critical Traps and Edge Cases

Trap 1: Modifying String Literals vs. Local Character Arrays

Consider these two seemingly identical string declarations:

main.c
Run Online
/* CASE A: String Literal Pointer */char *str_ptr = "Hello";
str_ptr[0] = 'M';
/* CRASH: Segmentation fault / Access Violation */
/* CASE B: Local Character Array */char str_arr[] = "Hello";
str_arr[0] = 'M';
/* SUCCESS: Modifies array to "Mello" */

Why Case A Crashes:

"Hello" is compiled into the Read-Only Data (.rodata) or Text segment. str_ptr is a local pointer residing on the stack that stores the address of that read-only page. When the CPU attempts to execute a write operation to a page marked r-x or r--, the MMU generates a page fault, and the OS kills the process.

Why Case B Succeeds:

str_arr allocates 6 bytes directly within the current function's Stack Frame. During function prologue execution, the initial characters 'H', 'e', 'l', 'l', 'o', '\0' are copied from .rodata into that local stack memory. Because the stack has rw- permissions, modifying str_arr[0] is completely valid.


Trap 2: Returning Pointers to Local Stack Variables (Dangling Pointer)

main.c
Run Online
int *get_multiplier(void) {
    int factor = 10;
    return &factor;
    /* FATAL BUG: Returning pointer to out-of-scope stack variable */
}
int main(void) {
    int *p = get_multiplier();
    printf("Value: %d\n", *p);
    /* Undefined behavior! May print 10, garbage, or crash */
    return 0;
}

When get_multiplier() returns, its stack frame is popped. The memory address &factor is no longer reserved and will be overwritten by subsequent function calls (like printf).

The Solution: Use the heap or static storage:

main.c
Run Online
int *get_multiplier_safe(void) {
    int *factor = malloc(sizeof(int));
    if (factor) *factor = 10;
    return factor;
    /* Caller is responsible for free() */
}

Trap 3: Stack Overflow from Large Local Arrays

Beginners frequently allocate large arrays directly inside functions:

main.c
Run Online
void process_image(void) {
    char image_buffer[10 * 1024 * 1024];
    /* 10 Megabytes on the stack */
    /* ... */
}

On Linux, the default thread stack size is 8MB; on Windows, it is typically 1MB. Allocating a 10MB array exceeds stack capacity immediately, crashing past the OS guard page with a Stack Overflow.

Rule of Thumb:

  • Use the stack for small buffers, structures, and primitive scalars (a few kilobytes or less).
  • Use the heap (malloc) for any buffer exceeding 64KB or when the size is determined at runtime.

Trap 4: Variable Scope vs. Storage Duration

Never confuse scope (where a name is visible) with storage duration (where the variable lives in memory):

main.c
Run Online
void counter(void) {
    static int hit_count = 0;
    /* Scope: LOCAL to counter() only
                                 Storage: BSS / Data Segment (Permanent process lifetime) */
    hit_count++;
}

Even though hit_count cannot be accessed by name outside counter(), it does not live on the stack. Its address is fixed in the Data/BSS segment, and it preserves its state for the entire execution of the program.

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
int g_initialized = 100;
// Data Segment
int g_uninitialized;
// BSS Segment
int main() {
    int stackVar = 42;
    // Stack Segment
    int *heapVar =(int*)malloc(sizeof(int));
    // Heap Segment
    *heapVar = 999;
    printf("Address in Data Segment: %p\
",(void*)&g_initialized);
    printf("Address in BSS Segment:  %p\
",(void*)&g_uninitialized);
    printf("Address on Heap:         %p\
",(void*)heapVar);
    printf("Address on Stack:        %p\
",(void*)&stackVar);
    free(heapVar);
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Allocate large buffers (over 64 KB) on the heap (malloc) rather than the stack to prevent stack overflow crashes.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: Why does the BSS segment reduce compiled binary file size on disk?

A: The executable file only stores the byte size of the BSS segment, not the actual zeros. When the OS loads the binary into RAM, it allocates and zeroes that block of memory automatically.
Q

Q: What is a Stack Overflow in C?

A: When the stack grows too large (e.g. from infinite recursion or allocating huge arrays on the stack) and breaches its allocated boundary, colliding with protected memory.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations