Lesson 16 / 16
Memory Layout — Stack and Heap
A process's address space regions, how the call stack operates, allocation on the heap, and lifetime rules.
Contents
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.
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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.