Yasir Explains/Compiler Design/Intermediate Code Generation/Run-Time Storage Organization
Intermediate Code Generation

Run-Time Storage Organization

On this page

DefinitionThe Memory LayoutThe Activation Record (Stack Frame)Example — fib's Activation RecordKey Points
Intermediate Code Generation

Run-Time Storage Organization

How the target program's memory is partitioned at run time: code, static data, heap, and stack. See the classic layout, the anatomy of an activation record, and why a recursive function like fib keeps many activation records alive on the stack at once.

Definition

Run-time storage organization. At run time the executing target program's memory is partitioned into separate areas, each holding a different category of data — the generated instructions, fixed global/static data, dynamically allocated objects, and the per-call working storage for active procedures.

The compiler does not place every datum at a fixed address. Instead it decides which area each kind of value belongs to and computes relative offsets within that area; the actual base addresses are fixed only when the program is loaded and run.

The two interesting areas grow dynamically as the program runs: the stack holds storage for procedure calls (it shrinks and grows with the call/return pattern), and the heap holds data whose lifetime is not tied to any single call.

The Memory Layout

A typical run-time memory image is laid out as four areas. We use the convention that lower addresses are at the top and addresses increase downward.

Example
low address
┌───────────────────────────────────────────┐
│ Code (text) │ generated machine
│ │ instructions — fixed size
├───────────────────────────────────────────┤
│ Static │ global / static data —
│ │ fixed size, known at compile time
├───────────────────────────────────────────┤
│ Heap ↓ grows │ dynamically allocated objects
│ │ (new / malloc); grows DOWNWARD
│ │ toward higher addresses
├───────────────────────────────────────────┤
│ │
│ (free gap) │ the two ends grow into here
│ │
├───────────────────────────────────────────┤
│ Stack ↑ grows │ activation records for active
│ │ calls; grows UPWARD toward
│ │ lower addresses
└───────────────────────────────────────────┘
high address

What lives in each area:

  • Code (text) — the generated machine instructions. Its size is fixed at compile/link time.
  • Static — global variables and static data. Their sizes and addresses are known at compile time, so they get a fixed spot.
  • Heap — data allocated explicitly at run time (malloc, new) whose lifetime is not tied to a procedure call. Here it grows downward toward higher addresses.
  • Stack — one activation record per active procedure call. Here it grows upward toward lower addresses.

The heap and the stack grow toward each other into the shared free gap between them. This lets either area expand as needed; the program runs out of memory only when the two ends meet.

The Activation Record (Stack Frame)

Activation record (stack frame). The contiguous block of storage allocated for one activation of a procedure — that is, one call. Each call gets its own activation record; it is pushed on entry and popped on return.

Its fields, top to bottom in a common layout:

Example
┌───────────────────────────────────────┐
│ returned value │ result handed back to the caller
├───────────────────────────────────────┤
│ actual parameters │ arguments passed in by the caller
├───────────────────────────────────────┤
│ control link │ pointer to the caller's activation record
├───────────────────────────────────────┤
│ access link / saved machine status │ return address + saved registers
├───────────────────────────────────────┤
│ local data │ the procedure's local variables
├───────────────────────────────────────┤
│ temporaries │ compiler-generated temps for subexpressions
└───────────────────────────────────────┘

Each field in one line:

  • returned value — where this call leaves its result for the caller to read.
  • actual parameters — the argument values the caller placed here for this call.
  • control link — points to the caller's activation record (used to pop back on return).
  • access link / saved machine status — the return address to resume the caller, plus saved registers (and, in nested-scope languages, an access link to a non-local frame).
  • local data — storage for the procedure's local variables.
  • temporaries — slots for intermediate values the generated code needs while evaluating expressions.

Example — fib's Activation Record

Take the running program:

Example
int main() { int x = fib(3); print(x); }
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}

Each call to fib builds an activation record. fib has one parameter n, no locals, and needs temporaries to hold the two recursive results before adding them:

Example
Activation record for one call fib(n):
┌───────────────────────────────────────┐
│ returned value │ the int this fib returns
├───────────────────────────────────────┤
│ actual parameter: n │ e.g. 3, or 2, or 1 …
├───────────────────────────────────────┤
│ control link → caller's record │ back to whoever called this fib
├───────────────────────────────────────┤
│ return address + saved registers │ where to resume in the caller
├───────────────────────────────────────┤
│ (no locals) │
├───────────────────────────────────────┤
│ temp t1 = fib(n - 1) │ result of the first recursive call
│ temp t2 = fib(n - 2) │ result of the second recursive call
└───────────────────────────────────────┘
returns t1 + t2

Why several records coexist. Because fib is recursive, evaluating fib(n - 1) happens before fib(n - 2) returns to do its add — so multiple fib activations are alive on the stack at the same time, each with its own n. This is the control stack: the chain of currently active calls, growing on each call and shrinking on each return.

Example
Stack while computing fib(3), deep inside fib(3) → fib(2) → fib(1):
┌──────────────────────┐
│ main: x = fib(3) │ x not yet assigned
├──────────────────────┤
│ fib: n = 3 │ waiting on fib(2)
├──────────────────────┤
│ fib: n = 2 │ waiting on fib(1)
├──────────────────────┤
│ fib: n = 1 │ about to return 1 ◄── top of stack
└──────────────────────┘

Three separate fib records, three separate values of n (3, 2, 1), each with its own control link back to the caller. As each call returns, its record is popped and control follows the control link back to the waiting caller.

Key Points

  • Run-time memory is partitioned by data category: code, static, heap, stack.
  • Code and static are fixed size (known at compile time); heap and stack grow dynamically.
  • With lower addresses on top, the heap grows downward and the stack grows upward, toward each other into the shared free gap.
  • An activation record is the storage for one call: returned value, parameters, control link, return address + saved registers, locals, and temporaries.
  • The control link points to the caller's record, threading the active calls into the control stack.
  • A recursive procedure like fib keeps many activation records alive at once, each with its own copy of the parameters and temporaries.