What You Will Learn
The stack is where function calls happen. Every time a function is called, a new stack frame is pushed onto the stack. When the function returns, its frame is popped off. Understanding the stack is critical for debugging crashes, understanding recursion limits, and writing safe embedded code.
On x86-32 and x86-64 Linux, the stack lives at the high end of virtual memory and grows downward (toward lower addresses). A special CPU register called the stack pointer (SP) always points to the current top of the stack.
When a function is called → a new frame is allocated (SP decreases).
When a function returns → its frame is released (SP increases).
| High addr ↑ | |
| C runtime startup frames | C runtime frames (before main) |
| main() frame | Frame: main() argc, argv (args) key = 9973 (local static) p (pointer, on stack) |
| ↓ grows down | |
| doCalc() frame | Frame: doCalc() val (argument) t (local int) return address → main |
| square() frame | Frame: square() ← TOP (SP points here) x (argument) result (local int) return address → doCalc |
| Low addr ↓ | ↑ Stack Pointer (SP) |
| Item in Frame | Description | C Term |
|---|---|---|
| Local Variables | Variables declared inside the function body | automatic variables |
| Function Arguments | Values passed by the caller | int x in foo(int x) |
| Return Address | Address in caller to jump to when function returns | saved PC register |
| Saved Registers | CPU registers the called function must preserve for the caller | call linkage info |
Every process actually has two stacks:
|
User Stack
|
Kernel Stack
|
Code Examples
#include <stdio.h>
void level3(void)
{
int c = 300;
printf("level3: &c = %p\n", (void*)&c); /* lowest address */
}
void level2(void)
{
int b = 200;
printf("level2: &b = %p\n", (void*)&b);
level3();
}
void level1(void)
{
int a = 100;
printf("level1: &a = %p\n", (void*)&a);
level2();
}
int main(void)
{
int m = 0;
printf("main : &m = %p (highest address)\n", (void*)&m);
level1();
return 0;
}
/* Output (x86-64): addresses DECREASE with each call
main : &m = 0x7ffd4a2b3abc (highest)
level1: &a = 0x7ffd4a2b3a80
level2: &b = 0x7ffd4a2b3a50
level3: &c = 0x7ffd4a2b3a20 (lowest — stack grew down) */
#include <stdio.h>
/* BAD: returns address of local variable — DANGLING POINTER */
int* bad_function(void)
{
int local = 42; /* lives on stack frame of bad_function */
return &local; /* frame is gone after return! */
}
/* GOOD: returns address of static variable — lives forever */
int* good_function(void)
{
static int persistent = 42; /* lives in data segment */
return &persistent;
}
int main(void)
{
int *p1 = bad_function();
/* *p1 is UNDEFINED BEHAVIOUR — frame was already popped */
printf("bad result (undefined!): %d\n", *p1);
int *p2 = good_function();
printf("good result: %d\n", *p2); /* always safe */
return 0;
}
/* Lesson: NEVER return the address of a local variable.
The stack frame is deallocated when the function returns.
Use static, malloc, or pass a buffer from the caller. */
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
static int depth = 0;
void stack_overflow_handler(int sig)
{
/* Reached when the stack is exhausted */
printf("\nSIGSEGV caught at recursion depth ~%d\n", depth);
printf("Stack overflow: stack frames filled available stack space.\n");
exit(1);
}
void recurse(void)
{
char local_buf[1024]; /* Each frame wastes 1 KB */
depth++;
(void)local_buf; /* Prevent optimisation */
recurse(); /* Infinite recursion — no base case */
}
int main(void)
{
signal(SIGSEGV, stack_overflow_handler);
printf("Starting deep recursion (each frame = 1 KB)...\n");
recurse();
return 0;
}
/* Output:
Starting deep recursion (each frame = 1 KB)...
SIGSEGV caught at recursion depth ~8120
Stack overflow: stack frames filled available stack space.
Default stack limit is ~8 MB → 8192 KB / 1 KB ≈ 8192 frames */
Interview Preparation Questions
Downward — from high virtual addresses toward lower addresses. Each new stack frame is allocated at a lower address than the previous one. The stack pointer (RSP on x86-64) is decremented as the stack grows.
A stack frame is a region on the stack allocated for one function call. It holds: the function’s local (automatic) variables, function arguments, the return address (where to resume after the function returns), and saved CPU registers.
When a function returns, its stack frame is popped — the memory is no longer valid for that variable. The returned pointer becomes a dangling pointer. Using it is undefined behaviour: the memory may be reused for the next function call’s frame, leading to silent data corruption or a crash.
Stack overflow happens when the stack grows past its limit (default ~8 MB). Causes: infinite recursion, very deep recursion, or allocating large arrays on the stack. Prevention: use iteration instead of deep recursion, allocate large buffers with malloc (heap), and increase the stack limit with ulimit -s if needed.
The user stack is in user-space virtual memory and holds frames for C functions. The kernel stack is a separate, protected, per-process stack in kernel memory used when executing system calls. The kernel cannot use the user stack because it resides in unprotected memory.
An automatic (local) variable lives on the stack and is created when the function is called and destroyed when it returns. A static variable lives in the data/BSS segment for the entire lifetime of the process — it persists across function calls and retains its value between invocations.
