6.5 The Stack and Stack Frames

6.5 The Stack and Stack Frames
Linux System Programming — Chapter 6
3
Core Concepts
3
Code Examples
6
Interview Questions

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.

How the Stack Works

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).

Stack Growth During main() → doCalc() → square() Call Chain
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)

What’s Inside a Stack Frame?
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
Key difference — automatic vs static: Local (automatic) variables live only as long as the function’s stack frame exists. Static and global variables live for the entire lifetime of the process.

User Stack vs Kernel Stack

Every process actually has two stacks:

User Stack
  • Lives in user-space virtual memory
  • Holds frames for your C functions
  • Grows downward from high addresses
  • Default ~8 MB limit (ulimit -s)
Kernel Stack
  • Lives in kernel memory (protected)
  • Used when a system call is executing
  • One per process, small (8-16 KB)
  • User code cannot access it

Code Examples

Example 1: Watch Stack Frame Addresses Grow Downward
#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) */
Example 2: Automatic Variable Lifetime — Gone When Function Returns
#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. */
Example 3: Stack Overflow via Deep Recursion
#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

Q1. In which direction does the stack grow on x86 Linux?

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.

Q2. What is a stack frame? What does it contain?

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.

Q3. Why is returning the address of a local variable dangerous?

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.

Q4. What causes a stack overflow and how can you prevent it?

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.

Q5. What is the difference between the user stack and the kernel stack?

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.

Q6. What is the difference between an automatic variable and a static variable?

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.

Leave a Reply

Your email address will not be published. Required fields are marked *