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.
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 value42is 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
counteris a file-scope global with an initializer: .data.localis an automatic-storage variable: stack (insidef’s frame).call_countisstaticwith a zero initializer: .bss. It keeps its value across calls tofbecause it has static storage duration.heapis itself a local variable, so the pointer variable lives on the stack. The block it points at (returned bymalloc) 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
- B is wrong:
mallocreturns uninitialized bytes (ISO §7.22.3.4). If you need zeros, usecallocormemset. - 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.