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:
- Arguments: The specific parameters passed to that execution layer (e.g.,
count). - Return Addresses: Instruction pointers mapping where to go once the layer finishes.
- Local Variables: Isolated blocks of stack memory dedicated strictly to that specific call instance.
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?
show_addressesnow takes anint countparameter — the recursion counter.- A base case (
if (count == 0) return;) — without this, the stack would grow until the OS kills the process (a stack overflow). - Three heap allocations per call — to observe heap address recycling patterns.
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:
- Code (Text) Segment:
0x568fdb9ac1c9— the compiled, read-only executable instructions. One copy, shared by every call. - Initialized Data Segment:
global_var(...f010) andstatic_var(...f014) — these exist for the entire program lifetime. - BSS Segment:
global_bssat...f01c— zero-initialized by the OS beforemain()runs.
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 (9018 → 8fc8 → 8f78) 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).