Functions and the call stack · about 50 min

The call stack and stack frames

Why this matters

You now know that each call gets fresh parameters and local variables, and that return somehow knows where to go back to. The mechanism behind both is the call stack, a region of memory that every C program uses on every call. Understanding it explains a lot at once: why local variables disappear when a function returns, why returning the address of a local variable is a classic bug, how recursion is even possible, what a debugger's "backtrace" shows, and what the famous "stack overflow" really is. It also prepares the ground for pointers and dynamic memory, where the difference between the stack and the heap becomes crucial.

A stack of frames

The process memory diagram from module 1 had a region called the stack. Every time a function is called, a block of memory called a stack frame (or activation record) is reserved on top of the stack for that call. When the function returns, its frame is released. Because the most recently called function is always the first to return, frames are added and removed in last-in, first-out order, like a stack of plates.

A frame typically holds:

The CPU keeps the address of the top of the stack in a register, the stack pointer (rsp on x86-64). Reserving a frame is just subtracting from the stack pointer; releasing it is adding back. That is why calling a function and creating local variables is extremely cheap: no searching for free memory, just arithmetic on one register.

Watching it happen

Take this program:

#include <stdio.h>

int inner(int z)
{
    int local_inner = z * 10;
    return local_inner;
}

int middle(int y)
{
    int local_middle = y + 1;
    return inner(local_middle);
}

int main(void)
{
    int local_main = 1;
    printf("result = %d\n", middle(local_main));
    return 0;
}

Here is the stack at each step. On Linux x86-64 the stack grows downward, toward lower addresses, so the newest frame is drawn at the bottom of the region but is called the "top" of the stack.

 1. in main            2. main calls middle(1)    3. middle calls inner(2)

 high addresses
 +----------------+    +----------------+         +----------------+
 | main           |    | main           |         | main           |
 |  local_main=1  |    |  local_main=1  |         |  local_main=1  |
 +----------------+    +----------------+         +----------------+
        ^ rsp          | ret -> main    |         | ret -> main    |
                       | middle         |         | middle         |
                       |  y=1           |         |  y=1           |
                       |  local_middle=2|         |  local_middle=2|
                       +----------------+         +----------------+
                              ^ rsp               | ret -> middle  |
                                                  | inner          |
                                                  |  z=2           |
                                                  |  local_inner=20|
                                                  +----------------+
 low addresses                                           ^ rsp

 4. inner returns 20: its frame is popped, rsp moves back up, middle resumes
 5. middle returns 20: its frame is popped, main resumes and prints "result = 20"

You can see the downward growth by printing the addresses of the locals (&x is the address of x, and %p prints an address; both are covered properly in the pointers module):

printf("main:   &local_main = %p\n", (void *)&local_main);

One run in the sandbox printed:

main:   &local_main = 0x7fff4ea1824c
middle: &local_middle = 0x7fff4ea1822c
inner:  &local_inner = 0x7fff4ea181fc
result = 20

The exact addresses change on every run (Linux randomizes where the stack starts, a security feature called ASLR), but each deeper call's locals sit at lower addresses: 0x...24c, 0x...22c, 0x...1fc.

The return address

How does return know where to go? The machine instruction call inner does two things: it pushes the address of the next instruction (the return address) onto the stack, then jumps to inner. At the end, the ret instruction pops that address off the stack and jumps to it.

 middle:
     ...
     call inner        ; push address of the next line, jump to inner
     mov  ...          ; <- the return address: execution resumes here

The return address sits in the frame right next to the local variables. Remember that: in the arrays module you will see what happens when a program writes past the end of a local array and overwrites it. Overwriting return addresses was the basis of the classic "stack smashing" exploits.

Why locals disappear

When a function returns, its frame is released simply by moving the stack pointer back. The bytes are not erased; they are just no longer reserved, and the next call will reuse that memory for its own frame. That is the precise meaning of lifetime: a local variable exists from the start of its block until the block ends. After that, its memory belongs to whoever is called next.

This gives two rules you must never break:

The debugger's view: backtraces

A debugger can walk the chain of frames, using the saved return addresses and frame pointers, and show you the whole path of calls that led to the current line. With gdb, stopped inside inner:

(gdb) bt
#0  inner (z=2) at v.c:5
#1  0x0000558c83a171ac in middle (y=1) at v.c:14
#2  0x0000558c83a171e2 in main () at v.c:21

Frame #0 is the running function. Each line below is a caller, paused at the line where it made the call, with its parameter values. When a program crashes, the backtrace is the first thing to look at. (The -g flag in our build command is what lets gdb show file names, line numbers and variable names. The debugging module teaches gdb properly.)

Stack overflow

The stack is not infinite. On Linux the default limit is 8 MB per program (ulimit -s prints 8192, in kilobytes). Each frame takes some of it: a few dozen bytes for a small function, more for one with large local arrays. If calls nest too deeply, the stack runs into the end of its region, the kernel detects an access to memory the program does not own, and kills the process with a segmentation fault.

A function that calls itself forever does exactly that. Here each frame contains a 1000-byte local array (arrays come in the next module), so the 8 MB stack fills after about 8000 calls:

depth 7000
depth 8000
Segmentation fault (core dumped)

The shell reports exit status 139, which is 128 + 11: killed by signal 11, SIGSEGV. gcc even warns about the obvious case at compile time:

w.c:5:6: warning: infinite recursion detected [-Winfinite-recursion]

This is where the name of a certain programming Q&A site comes from. Deep but finite recursion and large local arrays are the usual real-world causes. The next lesson, on recursion, shows how to make sure a recursive function stops.

Common mistakes

Recap

Going further: compile the three-function example with -g, then in the sandbox terminal run gdb ./prog, type break inner, run, bt, and info frame. Try up and print local_middle to look at a caller's frame.

Exercises

Exercise 1 · quiz

Who is on top?

main calls load_config, which calls parse_line, which calls trim. While trim is running, which frames are on the call stack, from the top (most recent) down?

Answer
  • trim, parse_line, load_config, main

Every function that has been called and has not yet returned still has a frame on the stack. The newest frame, trim's, is on top; main's, the oldest, is at the bottom. Each frame below is paused in the middle of a call, waiting for the one above it to return. This is exactly the list a debugger prints with bt (backtrace).

Exercise 2 · quiz

What lives in a stack frame?

Which of these are typically stored in a function's stack frame (at -O0 on x86-64)? Select all that apply.

Answer
  • The function's local variables

  • Copies of its parameters

  • The return address: where to continue in the caller

A frame holds the return address (pushed by the call instruction), the caller's saved frame pointer, the function's local variables and, at -O0, copies of its parameters (which arrive in registers on x86-64 and are stored into the frame). Machine code lives in the read-only code region, shared by all calls; globals live in the data region. Neither is copied per call.

Exercise 3 · write code

Watch the stack grow and shrink

Make the call stack visible. Write three functions, outer, middle and inner, each taking an int depth parameter. Each one prints > name when it starts and < name just before it returns, indented by 2 * depth spaces. outer calls middle(depth + 1) between its two prints, and middle calls inner(depth + 1). main calls outer(0) and then outer(0) again.

The required output is exactly:

> outer
  > middle
    > inner
    < inner
  < middle
< outer
> outer
  > middle
    > inner
    < inner
  < middle
< outer

A helper indent(depth) that prints the spaces is provided.

File: main.c

Starting point

#include <stdio.h>

void indent(int depth)
{
    for (int i = 0; i < 2 * depth; i++)
        putchar(' ');
}

void outer(int depth)
{
    indent(depth);
    printf("> outer\n");
    indent(depth);
    printf("< outer\n");
}

int main(void)
{
    outer(0);
    outer(0);
    return 0;
}

Example

expected output

> outer
  > middle
    > inner
    < inner
  < middle
< outer
> outer
  > middle
    > inner
    < inner
  < middle
< outer
Hint 1

Define inner first, then middle, then outer (or add prototypes), so each is declared before it is called.

Hint 2

middle looks like outer, with inner(depth + 1); between its two prints.

Reference solution
#include <stdio.h>

void indent(int depth)
{
    for (int i = 0; i < 2 * depth; i++)
        putchar(' ');
}

void inner(int depth)
{
    indent(depth);
    printf("> inner\n");
    indent(depth);
    printf("< inner\n");
}

void middle(int depth)
{
    indent(depth);
    printf("> middle\n");
    inner(depth + 1);
    indent(depth);
    printf("< middle\n");
}

void outer(int depth)
{
    indent(depth);
    printf("> outer\n");
    middle(depth + 1);
    indent(depth);
    printf("< outer\n");
}

int main(void)
{
    outer(0);
    outer(0);
    return 0;
}

Exercise 4 · quiz

Read a backtrace

A program crashed and gdb's bt command shows:

#0  compute_average (count=0, total=57) at stats.c:12
#1  0x000055555555520e in summarize (n=0) at stats.c:25
#2  0x0000555555555262 in main () at stats.c:40

Which statement is correct?

Answer
  • main called summarize at line 40; summarize called compute_average at line 25; the crash is at line 12 of compute_average, where count is 0

Frame #0 is the innermost: the function that was running when the program stopped, at the line shown. Each lower-numbered frame was called from the line listed in the frame below it. Here count=0 at line 12 strongly suggests a division by zero (integer division by zero kills the process with SIGFPE). The debugging module shows how to get here yourself with gdb.