student@ubuntu:~$
pointers-memory Lesson 7 10 min read

Stack, Heap & Memory Layout

Five segments, two lifetimes, and why you cannot return a pointer to a local

Based on content from Dr. Stu Steiner, Eastern Washington University.

Reading: Hanly & Koffman: §13.2 (pp. 711–716); K&R §4.1 (pp. 67–70), §7.8.5 (pp. 167–169)

In a nutshell

A running C program partitions its virtual address space into five conventional regions: text (compiled machine code), data (initialized globals), bss (zero-initialized globals), stack (local variables and call frames), and heap (memory you asked for with malloc). The language does not define this layout; every mainstream platform agrees on it, and every C textbook teaches it, because lifetime rules are organized by segment. A stack local is leased for the lifetime of one function call. A heap allocation is leased until you call free. Confusing the two is the root cause of the classic “return a pointer to a local” bug: the pointer survives, the memory does not. This lesson is the vocabulary you need to read the next one (where malloc and free actually appear).

Practice this topic: Memory Layout drill, or browse the practice gallery.

After this lesson, you will be able to:

  • Name the five memory segments and the lifetime rule for each
  • Draw a stack frame and say what lives inside it
  • Explain why return &local_var; is undefined behavior
  • Contrast stack lifetime (scope-bound) with heap lifetime (free-bound)
  • Predict which segment a given variable lands in, based on how it is declared

Quick reference

Segment What lives there Lifetime Initialized?
text compiled machine code, string literals entire program read-only
data initialized globals and static vars entire program compile-time value
bss zero-initialized globals and static vars entire program zeroed at startup
stack locals, parameters, return addresses one function call indeterminate
heap malloc / calloc / realloc blocks until you free malloc leaves garbage

Coming from CSCD 210

Java hides all of this behind a single abstraction: “new goes on the heap; the garbage collector cleans up later.” You never returned a Java array from a method and worried that the array’s memory was reclaimed when the method returned, because Java arrays are always heap-allocated and the GC keeps them alive as long as any reference points at them. C removes both guarantees. An array declared inside a function is a stack array; its storage vanishes the instant the function returns. Heap storage is only reclaimed when you call free. That is the tradeoff: C gives you the choice of where memory lives and how long, and hands you the bill for managing it.


The five memory segments

When the OS loader maps your executable into memory, it carves the process’s address space into five conventional regions. Low addresses at the bottom, high addresses at the top.

  high addresses
  +--------------------------+
  |          STACK           |   grows downward
  |            |             |
  |            v             |
  |                          |
  |    (unmapped region)     |
  |                          |
  |            ^             |
  |            |             |
  |           HEAP           |   grows upward
  +--------------------------+
  |       .bss               |   zero-init globals / statics
  +--------------------------+
  |       .data              |   init'd globals / statics
  +--------------------------+
  |       .text              |   compiled code, string literals
  +--------------------------+
  low addresses

Stack and heap grow toward each other through the unmapped region between them. On modern 64-bit Linux that middle region is enormous, so collisions are rare; on 32-bit systems they were a real concern.

text: compiled code and string literals

Every function you write ends up as CPU instructions in the text segment. The OS marks these pages read-only and executable. Writing through a function pointer fails; jumping to a text address runs code. String literals ("hello") also usually live here, which is why char *s = "hello"; s[0] = 'H'; is undefined behavior: you are trying to write to a read-only page.

data and bss: globals and statics

Globals (variables declared outside any function) and static variables (declared with the static keyword) exist for the entire program run. They split by whether you gave them an initializer:

  • int counter = 42; goes into .data. The value 42 is stored in the executable file.
  • int seen[1024]; (at file scope) goes into .bss. The executable records only the size; the OS zeroes these bytes at program startup. That zero-initialization is guaranteed by ISO C §6.7.9, and it is the one place C gives you pre-zeroed memory for free.

stack: automatic storage

Every function call pushes a fresh stack frame holding that call’s parameters, local variables, saved registers, and the return address. When the function returns, the frame is popped: the CPU’s stack pointer moves back, and those bytes are available for the next call to overwrite.

Local variables have what ISO C §6.2.4 calls automatic storage duration: they come into existence when control enters their block and cease to exist when control leaves. You never explicitly allocate or free them. The compiler emits a couple of arithmetic instructions on the stack pointer, and the frame exists or it is gone.

Two consequences. First, stack allocation is essentially free at runtime, which is why recursion is cheap. Second, the stack has a hard size limit: on Linux the default is 8 MiB (ulimit -s reveals yours). A local like char buf[10000000]; overflows it and crashes your program with SIGSEGV.

heap: allocated storage

The heap is the one region where you decide when a block’s lifetime begins and ends. You call malloc (or calloc, realloc); the allocator hands you a pointer to a block of bytes; that block is yours until you call free on it. Not until the function returns. Not until the program ends. Until you give it back.

The heap has no scope. A pointer returned from malloc inside a helper function remains valid after the helper returns, across function boundaries, across threads, as long as nobody calls free on it. That is exactly the property that solves the “return an array from a function” problem coming up in the next section.

Check your understanding (which segment?)

For each declaration, predict which segment the variable itself lives in.

int counter = 42;                  /* file scope            */

void f(void)
{
    int local = 10;                /* inside f              */
    static int call_count = 0;     /* static inside f       */
    int *heap = malloc(4);         /* heap pointer variable */
    /* ... */
    free(heap);
}
Reveal answer
  • counter is a file-scope global with an initializer: .data.
  • local is an automatic-storage variable: stack (inside f’s frame).
  • call_count is static with a zero initializer: .bss. It keeps its value across calls to f because it has static storage duration.
  • heap is itself a local variable, so the pointer variable lives on the stack. The block it points at (returned by malloc) lives on the heap. Two different locations; students often conflate them.

Stack lifetime: the “return a pointer to a local” bug

Here is the problem that makes this lesson necessary. Suppose the user types n and you want to build an array of n integers and hand it back to your caller. First instinct, coming from Java:

int *make_array(int n)
{
    int arr[n];                             /* lives on make_array's stack */
    for (int i = 0; i < n; i++) arr[i] = i;
    return arr;                             /* WRONG */
}

Compile with -Wall and the compiler tells you: warning: function returns address of local variable [-Wreturn-local-addr]. Why is this a bug?

arr lives in make_array’s stack frame. The frame exists exactly as long as make_array is running. The moment make_array returns, the stack pointer moves back and those bytes are “available” for the next function call. The address is still a valid 64-bit number; it looks fine when you print it; sometimes it even reads back the original values if nothing has overwritten it yet. Then you call printf in the caller, printf’s stack frame lands on top of those bytes, and your array turns into garbage.

During make_array(3):              After make_array returns:

  caller frame                       caller frame
    +---+                              +---+
    | p | ----+                        | p | -----+
    +---+    |                         +---+     |
             v                                   v
  make_array frame                     (reclaimed memory)
    +---+ +---+ +---+                    +---+ +---+ +---+
    | 0 | | 1 | | 2 |                    | ? | | ? | | ? |
    +---+ +---+ +---+                    +---+ +---+ +---+

The pointer looks valid. The memory it points to does not belong to anyone in particular. Dereferencing it is undefined behavior (ISO C §6.2.4). What makes the bug nasty is that the tests you ran on your laptop may pass; the tests run by the autograder, with different timing and different stack layout, may fail.

The fix: put it on the heap

#include <stdlib.h>

int *make_array(int n)
{
    int *arr = malloc(n * sizeof(int));
    if (arr == NULL) {
        return NULL;
    }
    for (int i = 0; i < n; i++) arr[i] = i;
    return arr;                             /* valid: heap outlives frames */
}

int main(void)
{
    int *a = make_array(5);
    if (a == NULL) return 1;
    /* ... use a ... */
    free(a);                                /* caller's responsibility */
    return 0;
}

The heap block’s lifetime is not tied to any stack frame, so the returned pointer is valid until somebody calls free. That “somebody” is whoever wrote the code that now owns the pointer. The full rules for malloc and free are next lesson; for now the point is the lifetime contrast, not the function’s signature.

Check your understanding (predict the failure mode)

int *f(void)
{
    int x = 5;
    return &x;
}

int main(void)
{
    int *p = f();
    printf("%d\n", *p);
    printf("%d\n", *p);
    return 0;
}

Will this compile? Will it print 5 twice? What does the standard say?

Reveal answer

It compiles (with a -Wreturn-local-addr warning). It may print 5 5, 5 followed by garbage, or crash. All three are valid outcomes of undefined behavior.

The first printf has good odds of printing 5: nothing between f returning and the printf call has overwritten the stack slot that used to hold x. The second printf is much less likely to print 5: the first printf itself almost certainly wrote over those bytes while running.

The C standard (§6.2.4) says the lifetime of x ends when f returns, and access to an object outside its lifetime is undefined. “Undefined” means the implementation is not required to warn you, crash, or behave consistently.


Heap lifetime, briefly

Full mechanics come next lesson. Two ideas to seed now, because they turn up in Lab 6 on day two.

The heap’s gift is also its bill. Every malloc block you allocate must be matched by exactly one free call. Zero frees leaks memory; two frees on the same pointer corrupts the allocator. Use-after-free (reading or writing through a pointer after you freed it) is undefined behavior and the source of half the security vulnerabilities you have ever heard about.

The pointer outlives the allocation. After free(p);, the pointer variable p still holds the same address; the memory at that address no longer belongs to you. A pointer in this state is called dangling. The defensive habit is p = NULL; right after free, so a later accidental use becomes a clean crash instead of silent corruption.

int *p = malloc(sizeof(int));
if (p == NULL) return 1;
*p = 42;
free(p);
p = NULL;          /* makes any later *p a deterministic trap */

For the full machine-level picture of how stack frames, the ELF loader, and ASLR fit together, see the machine model deep dive. For the heap side (what malloc asks the OS for, how the free list works, why free(NULL) is a no-op), see the pointer mechanics deep dive.


What comes next

Which statements are correct?
AA local variable's storage is reclaimed when its function returns, even if a pointer to it escaped.
Bmalloc zero-initializes the bytes it returns.
CA static variable declared inside a function keeps its value across calls because it has static, not automatic, storage duration.
DWriting through a pointer to a string literal is undefined behavior because string literals usually live in read-only .text.
EThe heap lives at higher addresses than the stack on Linux x86-64.
Correct: A, C, D.
  • B is wrong: malloc returns uninitialized bytes (ISO §7.22.3.4). If you need zeros, use calloc or memset.
  • E is wrong: the stack sits at high addresses and grows down; the heap sits at low addresses (just above .bss) and grows up. They approach each other through the unmapped middle.

Next, Dynamic Allocation turns the heap from a concept into code: malloc’s signature, the canonical allocation idiom, why you check the return, what calloc and realloc add, and what free does and does not do. Drill this page with the Memory Layout skill card, or browse the practice gallery.