The stack and the heap

Automatic variables live in a stack frame that exists only while the function runs; static objects live for the whole program; heap objects live from allocation to release. The three regions have different addresses, different lifetimes and different failure modes. This lesson shows the first two and opens the third, which the next lessons take further.

Stack frames

Every call pushes a frame holding the parameters, the return address and the local variables. The addresses of locals in nested calls show the direction: on x86_64 the stack grows downward, so each deeper frame sits at a lower address. The exact values change per run and per build.

#include <stdio.h> static void inner(int depth) { int local = depth; printf("depth %d: local %p\n", depth, (void *) &local); } static void outer(void) { int local = 1; printf("depth 1: local %p\n", (void *) &local); inner(2); } int main(void) { int local = 0; printf("depth 0: local %p\n", (void *) &local); outer(); return 0; }

gcc -Wall -Wextra frames.c -o frames
./frames

depth 0: local 0x7ffd59e13e54
depth 1: local 0x7ffd59e13e34
depth 2: local 0x7ffd59e13e14

Each frame is 32 bytes lower than the previous one in this build. The numbers are addresses of automatic objects, so copying them into a variable that outlives the call is the mistake at the end of the lesson.

Automatic and static lifetime

A local variable is created on entry and destroyed on return; a static object is placed in a different region before main starts and keeps its address. The heap is a third region, managed explicitly, and its addresses are far from both. The program prints one address from each region.

#include <stdio.h> #include <stdlib.h> static int global; int main(void) { int local; int *heap = malloc(sizeof *heap); if (heap == NULL) { return 1; } *heap = 7; printf("static: %p\n", (void *) &global); printf("local: %p\n", (void *) &local); printf("heap: %p (%d)\n", (void *) heap, *heap); free(heap); return 0; }

gcc -Wall -Wextra where.c -o where
./where

static: 0x5654b7b2d070
local: 0x7ffece664a7c
heap: 0x7f385ffeccb0 (7)

The static address is near the program code, the local one is on the stack, and the heap address is in a separate mapping. The values differ on every run because of address space randomisation; the ordering of the regions is what repeats.

The heap

malloc returns a block whose lifetime is not tied to any function. It stays valid until free is called, which is why a function can build an object and hand it to its caller. The allocation must be checked for NULL, and every successful allocation needs exactly one matching free.

#include <stdio.h> #include <stdlib.h> static int *make_number(int value) { int *number = malloc(sizeof *number); if (number != NULL) { *number = value; } return number; } int main(void) { int *a = make_number(11); int *b = make_number(22); if (a == NULL || b == NULL) { free(a); free(b); return 1; } printf("%d %d\n", *a, *b); free(a); free(b); return 0; }

gcc -Wall -Wextra heap.c -o heap
./heap

11 22

The two numbers survive the return from make_number because they are not in its frame. free(NULL) is allowed, which is why the failure path can free both pointers without another test.

Returning a local address

A pointer to a local variable points into a frame that disappears when the function returns. The compiler sees the pattern and warns, because the caller would read a frame that another call may already have reused:

#include <stdio.h> static int *bad_pointer(void) { int local = 5; return &local; } int main(void) { int *p = bad_pointer(); printf("%d\n", *p); return 0; }

gcc -Wall -Wextra retlocal.c -o retlocal

retlocal.c: In function 'bad_pointer':
retlocal.c:7:12: warning: function returns address of local variable [-Wreturn-local-addr]
7 | return &local;
| ^~~~~~

The program is not run here because the value it would read is whatever the next call puts in that memory. The fix is one of the alternatives this lesson introduced: return the value itself, return a pointer to a static object, or allocate the object on the heap and make the caller free it.

Exercises

  1. Print the addresses of two locals in the same function and of two parameters. How far apart are they, and why is the answer not fixed?
  2. Write a function that allocates an array of five ints on the heap, fills it, and returns the pointer. Free it in main.
  3. Explain why returning a pointer to a static variable works but is dangerous when the caller keeps two results from the same function.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. In which direction does the stack grow on x86_64?

    Answer

    Downward, toward lower addresses

    Each deeper call gets a lower address, which is why the depth-2 frame in the example sits below the depth-1 frame.

  2. How long does an automatic local variable live?

    Answer

    While its function call is active

    The frame exists from entry to return, so the object is destroyed when the function returns.

  3. Where does a static file-scope object live?

    Answer

    In a static region for the whole program

    Static storage duration keeps the object for the whole run, and its address is far from the stack and the heap.

  4. What must be checked after malloc?

    Answer

    That the pointer is NULL

    malloc returns NULL when it cannot satisfy the request, and using that pointer would be undefined.

  5. What does this program print? #include <stdio.h> #include <stdlib.h> static int *make_number(int value) { int *number = malloc(sizeof *number); if (number != NULL) *number = value; return number; } int main(void) { int *a = make_number(11); int *b = make_number(22); if (a == NULL || b == NULL) { free(a); free(b); return 1; } printf("%d %d\n", *a, *b); free(a); free(b); return 0; }

    Answer

    11 22

    The two allocated numbers keep their values after the function returns and are printed before they are freed.

  6. Which kind of object lives in the stack frame of a running function?

    Answer

    An automatic local variable

    Automatic locals live in the current frame, while static objects and heap blocks live in other regions.

  7. What does free(NULL) do?

    Answer

    It is allowed and does nothing

    The standard allows free(NULL), which lets a failure path release both pointers without an extra test.

  8. On x86_64, how do the addresses of automatic locals change as calls go deeper?

    Answer

    Each deeper frame has a lower address

    The stack grows downward on x86_64, so each deeper frame sits at a lower address.

  9. This program is built with gcc -std=c17 -Wall -Wextra retlocal.c -o retlocal. What does GCC warn about? #include <stdio.h> static int *bad_pointer(void) { int local = 5; return &local; } int main(void) { int *p = bad_pointer(); printf("%d\n", *p); return 0; }

    Answer

    function returns address of local variable [-Wreturn-local-addr]

    The frame disappears on return, so GCC warns about returning the address of an object that will not exist.

  10. Why is the value read through a returned local address indeterminate?

    Answer

    The frame may already be reused by another call

    Once the function returns, the memory is no longer reserved, and the next call can overwrite it with unrelated data.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev