Home

Memory Layout in C - Proving Stack Growth with Recursion

From Theory to Proof

In the previous post, we mapped out C’s memory segments and saw where different variables live. We noted that the stack grows downward and the heap grows upward, but with a single function call, we could only observe a snapshot.

This post goes further. Using recursion, we force multiple stack frames to exist simultaneously, giving us empirical proof of how the stack and heap behave at runtime.

Why Recursion Reveals the Stack

When a function finishes executing, its stack frame is immediately popped off and destroyed. A subsequent call simply reuses that exact same memory address — you’d never see the growth pattern.

Recursion changes this. By calling a function from within itself before the current execution finishes, the operating system must preserve the existing frame and push a brand-new one directly on top of it.

Every recursive depth level requires its own unique tracking state:

By chaining these frames together, recursion allows us to peek at multiple active stack frames simultaneously.


The Demonstration Program

Here’s a modified version of our earlier program, now using recursion to create multiple stack frames:

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

// Global variable - Data segment
int global_var = 42;
// Uninitialized global variable (BSS segment)
int global_bss;

void show_addresses(int count) {
    // Base case: prevents infinite recursion and Stack Overflow
    if (count == 0) {
        return;
    }
    // local variable - stack
    int local_var = 10;
    int second_local_var = 10;

    // static var -- Data segment
    static int static_var = 20;

    // dynamic variable - heap
    int *heap_var = malloc(sizeof(int));
    int *second_heap_var = malloc(sizeof(int));
    int *third_heap_var = malloc(sizeof(int));

    printf("Code (function) address:        %p\n", (void *)show_addresses);
    printf("Global variable address:        %p\n", (void *)&global_var);
    printf("Uninitialized global address:   %p\n", (void *)&global_bss);
    printf("Static variable address:        %p\n", (void *)&static_var);
    printf("Stack variable address:         %p\n", (void *)&local_var);
    printf("Second stack variable address:  %p\n", (void *)&second_local_var);
    printf("Heap variable address:          %p\n", (void *)heap_var);
    printf("Second heap variable address:   %p\n", (void *)second_heap_var);
    printf("Third heap variable address:    %p\n", (void *)third_heap_var);
    free(heap_var);
    free(second_heap_var);
    free(third_heap_var);
    printf("\n");

    // Recursive Step: Pushes a new frame onto the stack
    show_addresses(count - 1);
}

int main(void) {
    show_addresses(3);

    return 0;
}

What changed from the previous version?

  1. show_addresses now takes an int count parameter — the recursion counter.
  2. A base case (if (count == 0) return;) — without this, the stack would grow until the OS kills the process (a stack overflow).
  3. Three heap allocations per call — to observe heap address recycling patterns.
  4. free() before recursion — the blocks are released before the next recursive call allocates new ones.

Runtime Output

Code (function) address:        0x568fdb9ac1c9
Global variable address:        0x568fdb9af010
Uninitialized global address:   0x568fdb9af01c
Static variable address:        0x568fdb9af014
Stack variable address:         0x7ffe96459018
Second stack variable address:  0x7ffe9645901c
Heap variable address:          0x56901735c2a0
Second heap variable address:   0x56901735c2c0
Third heap variable address:    0x56901735c2e0

Code (function) address:        0x568fdb9ac1c9
Global variable address:        0x568fdb9af010
Uninitialized global address:   0x568fdb9af01c
Static variable address:        0x568fdb9af014
Stack variable address:         0x7ffe96458fc8
Second stack variable address:  0x7ffe96458fcc
Heap variable address:          0x56901735c2e0
Second heap variable address:   0x56901735c2c0
Third heap variable address:    0x56901735c2a0

Code (function) address:        0x568fdb9ac1c9
Global variable address:        0x568fdb9af010
Uninitialized global address:   0x568fdb9af01c
Static variable address:        0x568fdb9af014
Stack variable address:         0x7ffe96458f78
Second stack variable address:  0x7ffe96458f7c
Heap variable address:          0x56901735c2a0
Second heap variable address:   0x56901735c2c0
Third heap variable address:    0x56901735c2e0

Reading the Results

1. Fixed Segments (Code, Data, BSS) — Unchanged

The addresses for the function pointer, globals, and statics remain exactly the same across all three calls:

These segments don’t grow or shrink. They are allocated once when the program loads.

2. The Stack — Proving Downward Growth

Looking at local_var across the three recursive levels reveals a steady decrease in address values:

Call local_var Address Delta from Previous
1 0x7ffe96459018
2 0x7ffe96458fc8 -0x50 (80 bytes)
3 0x7ffe96458f78 -0x50 (80 bytes)

The decreasing values (90188fc88f78) prove empirically that the stack grows downward toward lower addresses on modern x86 architecture. Each recursive call pushes a new frame 80 bytes below the previous one.

Notice also that second_local_var is always 4 bytes above local_var (e.g., 9018 vs 901c) — adjacent local variables are packed tightly within the same frame.

3. The Heap — Upward Growth and Recycling

In Call 1, the heap blocks scale upward as memory is requested:

...c2a0 → ...c2c0 → ...c2e0

Each malloc() returns an address 32 bytes (0x20) higher than the last — the heap allocator moves upward.

But here’s the interesting part: because free() releases these blocks back to the memory manager before the recursive call, the allocator immediately recycles them in Call 2. Notice the reversed order in Call 2:

Call 1: c2a0, c2c0, c2e0  (allocated in order)
Call 2: c2e0, c2c0, c2a0  (reused in reverse — LIFO recycling)

The allocator uses a Last-In, First-Out (LIFO) approach — the block freed last in Call 1 (...c2e0) becomes the first block reused in Call 2.

By Call 3, the pattern repeats consistently, confirming that the heap allocator maintains a free-list and efficiently reuses recently freed blocks.


Summary

Memory Region Growth Direction Observed Behavior
Code (Text) Fixed Same address across all calls
Data / BSS Fixed Same address across all calls
Stack Downward Each recursive frame is at a lower address
Heap Upward New allocations get higher addresses; freed blocks are recycled (LIFO)

Recursion gave us a window into the dynamic behavior of the stack. The same technique can be pushed further — increase the recursion depth and watch the stack addresses march steadily downward until you hit the heap (or trigger a stack overflow).

Tags: CLow-LevelProgramming