---
title: 'Memory Layout — Stack and Heap'
source: 'https://academia.sh/en/courses/how-computers-work/memory-layout'
course: 'How Computers Work'
language: en
updated: '2026-08-17T18:08:06+00:00'
license: 'CC BY-SA 4.0'
---

# Memory Layout — Stack and Heap

A process's address space regions, how the call stack operates, allocation on the heap, and lifetime rules.

The loader placed the executable file's sections into the process's address space and
handed control to the program. This lesson opens up that address space: which data
sits where, and for how long does it live?

The question is practical. Why a function's local variable becomes invalid after the
function returns; why unbounded recursion ends in a crash; why allocated memory must
be freed — all of these are consequences of this layout.

## Regions of the Address Space

A process's address space is divided into regions with different behaviors:

```
  high addresses
  ┌────────────────────────────────┐
  │ Stack                          │  grows downward
  ├────────────────────────────────┤
  │              ...               │  free space
  ├────────────────────────────────┤
  │ Heap                           │  grows upward
  ├────────────────────────────────┤
  │ Uninitialized data             │  zeroed global variables
  ├────────────────────────────────┤
  │ Initialized data               │  global variables given a value
  ├────────────────────────────────┤
  │ Read-only data                 │  constants
  ├────────────────────────────────┤
  │ Code                           │  instructions; not writable
  └────────────────────────────────┘
  low addresses
```

The stack and the heap growing from opposite ends lets the two share the free space
between them: whichever needs more room expands into it.

The code region being closed to writes is a security measure: it prevents a running
program from modifying its own instructions. Likewise, data regions being closed to
execution prevents bytes placed as data from being executed as instructions — this
separation is one of the fundamental defenses against memory safety vulnerabilities.

## The Call Stack

When a function is called, a **stack frame** for that call is pushed onto the top of
the stack. A frame typically carries:

- **Return address:** The address of the instruction where control resumes when the
  function ends.
- **Arguments:** The ones that do not fit in registers.
- **Local variables:** The function's own variables.
- **Saved registers:** Register contents belonging to the caller that must be
  preserved.

One register — the stack pointer — holds the address of the top. On a call, the top
moves down; on a return, it moves back up. Because the only thing shifted is a
register value, allocating and releasing a frame is nearly free.

This layout fitting a stack structure is not a coincidence: calls are nested. The
function called last is the first to return. This is the answer to the question of
where the call and return instructions defined in the instructions lesson store the
return address.

The reason local variables become invalid when a function returns is also visible: the
frame has been released, and that space will be used by the next call. Returning the
address of a local variable means pointing at a location that another frame will
occupy shortly after.

## Stack Overflow

The stack is not infinite; it is created with a fixed upper limit per thread. The
limit is exceeded in two ways: a call chain that is too deep, or local data per frame
that is too large.

Recursion without a base case is the canonical example of the first case. Every call
adds a frame, none of them return, and the stack runs out.

```python
import sys

print(sys.getrecursionlimit())         # the limit set by the runtime

def depth(n: int = 1) -> int:
    """Calls itself until the limit is reached and returns the depth attained."""
    try:
        return depth(n + 1)
    except RecursionError:
        return n

print(depth())                         # a number close to the limit
```

The limit here is not the hardware's stack limit but a guard the runtime sets for
itself: a controlled error is raised before the real stack runs out and the process
crashes. In environments without this guard, the same program ends with the operating
system terminating the process.

Tail recursion, as it is defined in the Programming Fundamentals course, is one
solution to this problem: if the call is in tail position, the existing frame can be
reused instead of opening a new one. Not every implementation performs this
transformation.

## Stack per Thread

If more than one thread runs within a process, each must have its own stack: call
chains are independent, and one thread's frames cannot mix with another's.

The heap, on the other hand, is shared. This asymmetry sets up the fundamental tension
of concurrent programming: local variables on the stack are naturally isolated, while
heap data can be accessed by more than one thread at the same time. The need to
protect shared state arises from this, and concurrency is the subject of the Operating
System Concepts course.

There is also a practical consequence: the stack space allocated per thread limits the
number of threads that can be created.

## The Heap

Some data has to outlive the function that created it, and its size is known only at
run time. This data is allocated on the **heap**.

Heap management is done by an **allocator**. The allocator tracks free blocks; it
finds a block that fits the requested size, marks it, and returns its address. A freed
block becomes available for reuse.

This flexibility has three costs:

- **Allocation is expensive.** Searching for a suitable block involves far more work
  than shifting a stack pointer.
- **Fragmentation occurs.** Cycles of allocation and release can leave gaps that are
  sufficient in total but insufficient as a single block.
- **Lifetime is managed by hand.** A block that is never freed is a **memory leak**;
  an address used after it has been freed is a **dangling pointer**, and produces
  undefined behavior.

In languages that use a garbage collector, the third cost is handed off to the
runtime: objects that become unreachable are collected automatically. The cost of this
is the pauses that occur during collection and higher memory usage. The trade-off does
not disappear; it only shifts location.

## Lifetimes

Three separate lifetime rules correspond to three regions:

| Lifetime | Location | Start | End |
|---|---|---|---|
| Static | Data regions | While the program loads | When the program ends |
| Automatic | Stack | When the frame opens | When the function returns |
| Dynamic | Heap | On an allocation request | On release or collection |

A program's errors often arise from confusing these columns: accessing a value with
automatic lifetime after its lifetime has ended; never releasing a block with dynamic
lifetime; or modifying a shared value with static lifetime from multiple threads
concurrently.

## Tying the Course Together

At this point, the whole chain has been assembled. A program's text was tokenized,
turned into a tree, passed through an intermediate representation and translated into
machine instructions; the object files were linked, the loader placed the sections
into the address space, and it wrote the program counter to the entry point. The
processor began turning the fetch–decode–execute cycle; instructions operated on
registers, data moved through cache layers, external events were reported through
interrupts. And what all of these layers carried was the bit patterns that were the
subject of the first topic.

The pattern `0x41424344` was read five times across the course: as an unsigned
integer, a signed integer, a floating-point number, the text `ABCD`, and an
instruction. This is the course reduced to a single sentence: **memory carries no
meaning; meaning comes from the interpretation rule applied to the pattern.** A
programming language's type system, a file format's specification, and a network
protocol's documentation all do the same job — they state which rule applies.

## Summary

- A process's address space is divided into code, read-only data, initialized and
  uninitialized data, heap, and stack regions; the stack and the heap grow from
  opposite ends.
- Every function call pushes a frame onto the stack; the frame carries the return
  address, the arguments, and the local variables.
- Allocating a frame is a register shift; this makes it cheap, but the stack is
  bounded, and deep call chains exhaust it.
- The heap is for data whose size or lifetime is determined at run time; allocation is
  expensive, and fragmentation and lifetime management bring cost.
- Static, automatic, and dynamic lifetimes correspond to different regions; common
  memory errors arise from confusing the three.

## Next Step

This course built the layers beneath a program — data representation, the processor,
memory, and the compilation chain. The course after this one moves above that layer:
**Programming Fundamentals** takes up the concepts of variables, control flow,
functions, and recursion independently of any language. The code fragments used only
as examples in this course will there become the subject of the lesson itself.
