G27
Self-checkedTier 2 15 min readcd.runtime-storage-organisation

Storage organisation: static, stack and heap

Where the compiler puts the data of a running program: static storage fixed at compile time, the stack of activation records, and the heap; what each language feature forces (recursion needs a stack, not a heap; procedures returned as values and dynamic data structures need a heap; static allocation alone cannot support recursion), dangling references, the layout of a process, and how much stack a recursion uses.

Why it exists

The compiler must decide where every piece of data lives while the program runs, and the answer is not the same for all data: a global variable exists for the whole run, a local variable only while its procedure runs (and a recursive procedure has several copies at once), and a node allocated with malloc lives until the program frees it, whatever the procedure that allocated it did. The compiler organises memory into three kinds of storage for these three lifetimes. GATE asks which kind a given variable uses, which language features force the stack or the heap (and the classic wrong beliefs: recursion needs a heap, recursion needs garbage collection), and what goes wrong when the wrong kind is used.

Prerequisites

You need the activation record (also called a frame) that one call of a procedure occupies, with its parameters, locals and links, from activation records.

Precise definitions

Three kinds of storage.

StaticStackHeap
holdscode, constants, string literals, global and static variablesactivation records: parameters, local variables, return address, linksdynamically allocated objects (malloc, new)
address fixedat compile (or link or load) timeat run time, relative to the frame; offsets known at compile timeat run time, chosen by the allocator
lifetimethe whole runfrom the call to the return of the procedure: last in, first outfrom allocation until freed (or collected)
size knownat compile timethe size of a frame is known at compile time (except variable-sized arrays)at run time
costnone at run timea push and a pop: very cheapan allocator call, fragmentation, a free or a collection

The memory of a process is laid out with the code and the static data at the bottom, the heap above them growing towards higher addresses, and the stack at the top growing towards lower addresses (on most machines), with unused memory between them; the C memory layout page shows it, and the operating system maps it (process address space).

Static allocation. A compiler that allocates all data statically gives every procedure one fixed record. That works only if a procedure never has two activations alive at once, that is, without recursion (early Fortran), and without variable-sized arrays or dynamic data. With recursion the second activation would overwrite the first one's parameters and locals.

Stack allocation. A record is pushed when the procedure is called and popped when it returns. Because calls and returns nest, the records are freed in the reverse order of their creation, so a stack is enough: recursion is implemented with a stack, not with a heap, and needs no garbage collection. Nested procedures with static scoping add access links or a display to reach the records of enclosing procedures (the page on activation records). With dynamic scoping no heap is needed either.

Heap allocation. Needed for data whose lifetime is not nested in the calls: objects created by a procedure that outlive it (dynamic data structures: linked lists, trees), and the environment of a procedure that is returned as a value (a closure): the returned procedure refers to variables of the record in which it was created, and that record must survive the return of its procedure, so it cannot be on the stack. A language that allows a function to return a function as its result cannot use a pure stack discipline for its activation records.

Dangling reference. A reference to storage that has been freed or popped. Stack allocation makes it easy to create one: returning the address of a local variable (return &x;) refers to a record that is popped when the procedure returns; the same applies to a returned nested procedure that uses its creator's variables. Heap storage freed too early, or freed twice, causes the same errors.

Heap management. The allocator keeps a free list; free returns a block. Over time the free space is split into many small pieces (external fragmentation); allocators merge neighbouring free blocks and choose a block by first fit, best fit or similar. A language can free dead objects automatically: garbage collection (reference counting, mark and sweep, copying collection) finds the objects that can no longer be reached. It is needed to avoid leaks in a language with a heap and no explicit free; it is not needed for recursion.

Which feature needs what.

featureneeds
global variables, constants, literalsstatic storage
procedures that may call themselves (recursion)stack (a separate record per activation); not a heap
nested procedures, static scopestack plus access links or a display
arrays whose size is known at run time onlystack (variable-sized array in the record) or heap
dynamic data structures (malloc, new)heap
functions returned as values, with their environmentheap for the environment
dynamic scopingno heap needed
garbage collectiononly for languages that allocate on the heap without free

How it works

The same programs under three policies. A tiny language with recursion, nested procedures and procedure values is run three times: with static allocation (one record per procedure), stack allocation (a record is popped on return) and heap allocation (records are kept). Recursion fails with static allocation; a returned procedure fails on the stack ("dangling") and, with static allocation, two counters share one variable; the heap gets all of them right.

The same programs under three storage policies for activation records: start

Computed by a script, not typed
programstatic: one record per procedurestack: popped on returnheap: kept while referenced
fact(4), a recursive procedure
main calls make_counter once; the nested procedure inc, returned as a value, is called twice
main calls make_counter twice and uses both counters

Step 0

A tiny language with recursion, nested procedures and procedure values, run by the machine of scripts/cd_storage.py. Static: every activation of a procedure shares one record. Stack: a record is popped at the return and a later access to it is a dangling reference.

0 / 3
computed by script: programs run under three storage policies, code generated for a virtual machine and executed, liveness and exact searches for registers and spills. python3 scripts/gen-cd-t2-traces.py

How much stack does a recursion need? The space is the depth of the recursion, not the number of calls.

Recursion: calls made against records alive at once (fib): start

Computed by a script, not typed
callcalls in alldeepest chain = records alive at oncestack at 32 bytes per record
fib(1)
fib(2)
fib(3)
fib(5)
fib(8)
fib(10)
fib(15)

Step 0

Space on the stack is proportional to the DEPTH of the recursion, not the number of calls. The counts are the same as those measured by the C program of the page.

0 / 7
computed by script: programs run under three storage policies, code generated for a virtual machine and executed, liveness and exact searches for registers and spills. python3 scripts/gen-cd-t2-traces.py

Worked examples

EX 1Why static allocation cannot support recursion

Easy

The factorial is written twice. In the first, the parameter lives in one static variable (the single record of the procedure); in the second it is an ordinary parameter on the stack.

#include <stdio.h>
 
static int n_static;                         /* the one static record of fact_static: its parameter */
 
static int fact_static(int arg) {
    int r;
    n_static = arg;
    if (n_static <= 1) return 1;
    r = fact_static(n_static - 1);           /* the deeper activations overwrite n_static */
    return n_static * r;
}
 
static int fact_stack(int n) {
    int r;
    if (n <= 1) return 1;
    r = fact_stack(n - 1);
    return n * r;
}
 
int main(void) {
    printf("fact(4) with one static copy of the parameter: %d\n", fact_static(4));
    printf("fact(4) with automatic (stack) storage: %d\n", fact_stack(4));
    return 0;
}
fact(4) with one static copy of the parameter: 1
fact(4) with automatic (stack) storage: 24

Reading it. After fact_static(n - 1) returns, n_static holds 1 (the value written by the deepest call), so every level multiplies by 1 and the answer is 1. With automatic storage each activation has its own n, and the result is 4!=244! = 24. So static allocation of all data areas makes recursion impossible; dynamic allocation of activation records (the stack) is what makes it work. A heap and a garbage collector are not needed.

EX 2Space: calls against records alive at once

Medium

The recursive Fibonacci function makes exponentially many calls, but its stack is shallow. The program counts the calls and the deepest chain of active calls.

#include <stdio.h>
 
static long calls;
static int depth, deepest;
 
static long fib(int n) {
    long r;
    calls++;
    depth++;
    if (depth > deepest) deepest = depth;
    r = n < 2 ? 1 : fib(n - 1) + fib(n - 2);
    depth--;
    return r;
}
 
int main(void) {
    int ns[7] = {1, 2, 3, 5, 8, 10, 15};
    for (int i = 0; i < 7; i++) {
        calls = 0;
        deepest = 0;
        fib(ns[i]);
        printf("fib(%2d): %5ld calls, deepest chain %2d\n", ns[i], calls, deepest);
    }
    return 0;
}
fib( 1):     1 calls, deepest chain  1
fib( 2):     3 calls, deepest chain  2
fib( 3):     5 calls, deepest chain  3
fib( 5):    15 calls, deepest chain  5
fib( 8):    67 calls, deepest chain  8
fib(10):   177 calls, deepest chain 10
fib(15):  1973 calls, deepest chain 15

Reading it. fib(15) makes 1973 calls, but at no moment are more than 15 activations alive: each call returns before its sibling starts, so the stack holds only the chain from the original call down to the current one. With a 32-byte record the stack for fib(15) needs 15×32=48015 \times 32 = 480 bytes. The stack space of a recursion is proportional to its depth; the time is proportional to the number of calls. A recursion that goes a million levels deep (a naive list traversal) can overflow the stack although each frame is small.

EX 3Where does each variable live?

Medium

Three small tests of the three kinds of storage, and of the direction in which the stack grows on this machine.

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
 
static uintptr_t deepest_address;
 
static void deepest(int n) {
    volatile int local = n;                              /* one record per activation, on the stack */
    if (n == 0) deepest_address = (uintptr_t)&local;     /* the address is only compared, and only while the record exists */
    else deepest(n - 1);
}
 
static int next_static(void) {
    static int count = 0;                                /* static storage: one copy, kept between calls */
    return ++count;
}
 
static int next_automatic(void) {
    int count = 0;                                       /* automatic: a new copy in every activation */
    return ++count;
}
 
static int *make(void) {
    int *p = malloc(sizeof *p);                          /* heap: survives the return of make */
    if (p) *p = 42;
    return p;
}
 
int main(void) {
    volatile int outer = 0;
    deepest(5);
    uintptr_t a = (uintptr_t)&outer, b = deepest_address;
    printf("a local of a deeper activation has a %s address than one of its caller\n", b < a ? "LOWER" : "HIGHER");
    int s1 = next_static(), s2 = next_static(), s3 = next_static();          /* separate statements: the order of evaluating arguments is unspecified */
    int a1 = next_automatic(), a2 = next_automatic(), a3 = next_automatic();
    printf("static counter, three calls:    %d %d %d\n", s1, s2, s3);
    printf("automatic counter, three calls: %d %d %d\n", a1, a2, a3);
    int *h = make();
    if (!h) return 1;
    printf("heap block read after make() returned: %d\n", *h);
    free(h);
    return 0;
}
a local of a deeper activation has a LOWER address than one of its caller
static counter, three calls:    1 2 3
automatic counter, three calls: 1 1 1
heap block read after make() returned: 42

Reading it. The first line is a fact about this machine and its compiler, not the language: each deeper call puts its record at a lower address (the stack grows downwards on most machines). The static variable keeps its value between calls (one copy for the whole run), the automatic one starts again in every activation (a new record each time). The heap block was allocated inside make and is still valid after make has returned, because the heap is not tied to the calls. Which storage a variable gets follows from its declaration: global or static: static storage; local or parameter: the stack record; malloc: the heap.

EX 4Which statements are true?

GATE level

(1) A language without global variables and nested procedures, but with recursion, can be implemented with static allocation. False: recursion needs a record per activation. (2) Recursion cannot be implemented with dynamic allocation of activation records. False: that is exactly how it is done. (3) Automatic garbage collection is essential to implement recursion. False: a stack of records is freed by popping. (4) Both a heap and a stack are essential to implement recursion. False: the stack alone is enough. (5) A language that supports recursion necessarily needs heap storage. False. (6) Languages that allow dynamic data structures necessarily need the heap. True. (7) Languages that permit a function to return a function as its result cannot be implemented with a stack-based scheme alone. True: the environment must survive the return. (8) Static allocation of all data areas makes it impossible to implement recursion. True. (9) A multi-level access link (display) is needed only if the language has nested procedures. True. (10) Languages with dynamic scoping necessarily need a heap. False. A related fact from the same chapter: the symbol table is used in later phases too (type checking, code generation), and the rule "a variable must be declared before use" is checked in semantic analysis, not in syntax analysis.

EX 5Dangling references

GATE level

A procedure make_counter declares count, defines a nested procedure inc that increments count, and returns inc. After the call returns, inc is called. On the stack the record of make_counter (which holds count) has been popped, so the call to inc refers to freed storage: a dangling reference (the table: "DANGLING"). With heap allocation the record stays alive and the counter works: 12 for two calls in the first program. With static allocation it also works for one counter, but a second call to make_counter reuses the same record and resets the first counter's variable: two counters share one count and give 123 instead of 121. In C the same error is return &x; for a local x: the address refers to a frame that is gone.

Cheat box

Cheat box

  • Static: globals, static, constants, literals, code: address fixed at compile time, lives for the whole run, no recursion by itself.
  • Stack: activation records, LIFO, one record per call: this implements recursion, nested calls, local variables.
  • Heap: malloc and new; lifetime not nested in calls; closures that escape; needs free or garbage collection.
  • Recursion does not need a heap or garbage collection. Dynamic data structures do need the heap.
  • Returning a function with its environment ⇒\Rightarrow heap for the environment. Returning &local ⇒\Rightarrow dangling reference.
  • Stack space == depth of recursion ×\times frame size, not the number of calls.
  • Layout: code and static data low, heap grows up, stack grows down.

Traps and GATE patterns

Connections

The activation record of activation records is the unit that the stack holds; parameter passing decides what is copied into it. A C program sees the same three areas in the C memory layout, and malloc is the heap of dynamic memory; the operating system provides them as segments of the process address space.

PYQs

4 previous year questions from your book. I re-solved each one myself (0 by running or simulating, 4 by reasoning only); the book's key is shown next to my answer. All PYQs

GATE 2008 · MCQ · 1 markQ12 · page 253 of the book
Which of the following are true?
(I)
A programming language which does not
permit global variables of any kind and has
no nesting of procedures/functions, but
permits recursion can be implemented with
static storage allocation
(II)
Multi-level
access
link
(or
display)
arrangement is needed to arrange activation
records only if the programming language
being
implemented
has
nesting
of
procedures/functions.
(III) Recursion in programming languages cannot
be implemented with dynamic storage
allocation
(IV) Nesting procedures/functions and recursion
require a dynamic heap allocation scheme
and cannot be implemented with a stack-
based allocation scheme for activation
records
(V)
Programming languages which permit a
function to return a function as its result cannot
be implemented with a stack-based storage
allocation scheme for activation records.
  • (a)II and V only
  • (b)I, III and IV only
  • (c)I, II and IV only
  • (d)II, III and V only
GATE 2020 · MCQ · 1 markQ9 · page 274 of the book
Consider the following statements.

I.
Symbol table is accessed only during lexical
analysis and syntax analysis.

II.
Compilers for programming languages that
support recursion necessarily need heap
storage for memory allocation in the run-time
environment.

III.
Errors violating the condition 'any variable
must be declared before its use' are detected
during syntax analysis.

Which of the above statements is/are TRUE?
  • (a)II only
  • (b)I only
  • (c)I and III only
  • (d)None of I, II and III
GATE 2014 · MCQQ20 · page 283 of the book
Which of the following statements are Correct?
1. Static allocation of all data areas by a compiler
makes it impossible to implement recursion.
2. Automatic garbage collection is essential to
implement recursion.
3. Dynamic allocation of activation records is
essential to implement recursion.
4. Both heap and stack are essential to implement
recursion.
  • (a)1 and 2 only
  • (b)2 and 3 only
  • (c)3 and 4 only
  • (d)1 and 3 only
GATE 2010 · MCQ · 1 markQ27 · page 285 of the book
Which languages necessarily need heap allocation
in the runtime environment?
  • (a)Those that support recursion
  • (b)Those that use dynamic scoping
  • (c)Those that allow dynamic data structures
  • (d)Those that use global variables

Self-check

Question 1

Can recursion be implemented with only static allocation? With a stack but no heap? With a heap but no stack?

Show answer

Static only: no. Stack but no heap: yes. A heap alone can also work (each activation record allocated from it), but nothing requires it.

Question 2

Where is each of these stored: a global array, a local int, a malloced node, a string literal?

Show answer

Global array: static. Local int: stack frame. malloced node: heap. String literal: static (read-only) storage.

Question 3

fib(20) makes 21891 calls. How many activation records are alive at most at once?

Show answer

20 (the chain fib(20), fib(19), ..., fib(1)).

Question 4

Why can a function that returns a nested function not keep its activation record on the stack?

Show answer

The returned function uses variables of its creator's record; the record would be popped at the return and the later calls would use freed storage. The record must be kept (on the heap).

Question 5

Does a language with dynamic scoping need a heap?

Show answer

No. Dynamic scoping looks names up along the chain of callers, which are all on the stack.

Revision card

One-screen revision

  • Static: fixed for the run; stack: activation records, LIFO, recursion; heap: dynamic data, escaping closures, free or GC.
  • Recursion needs the stack, not a heap or a collector; static alone cannot support it.
  • Dangling reference: stack record popped (returned &local, escaping closure on the stack).
  • Stack space == depth ×\times frame size. Layout: heap grows up, stack grows down.

References

  • ·Aho, Lam, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (2nd edition), Chapter 7 Run-Time Environments (storage organisation, stack allocation of space, heap management, introduction to garbage collection)
  • ·Appel, Modern Compiler Implementation, Chapter 6 Activation Records
  • ·Louden, Compiler Construction: Principles and Practice, Chapter 7 Runtime Environments

Verification

The three storage policies are run on a tiny language with recursion, nested procedures and procedure values (scripts/cd_storage.py): the outcomes of the table (a recursive factorial is wrong under static allocation, a closure returned from a call is a dangling reference under stack allocation and wrong when two counters share one static record) are compared by scripts/enum-checks/cd_t2_check.py with direct models of each policy, with 200 random recursive procedures under stack and heap allocation (value and stack depth), and with the C programs printed on the page (a recursive factorial whose parameter lives in static storage prints 1, the same with automatic storage prints 24; the number of calls and the deepest chain of fib(n), counted by the program, equal the table); tests/cd-t2-traces.test.ts recomputes the table. That the stack grows towards lower addresses on the machine of the run is a property of the machine, printed by one program, not of the language. The statements about which language features need which storage are the standard textbook ones; garbage collection is described, not simulated. Textbook chapter titles are from memory. The independent second pass has not been run, so the page stays draft. SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and the numbers re-derived by hand or by a small script (token counts, FIRST and FOLLOW sets, LL(1) tables and moves, LR item sets, state counts and parses, operator-precedence values, attribute evaluation and printing schemes, activation-record counts and access links, parameter-passing results, three-address code, jumping and backpatched code, leaders and blocks, value numbering and DAGs, gen/kill and data flow sets, Sethi-Ullman labels and live ranges); long C programs were not re-read line by line (they are compiled and compared by npm run verify:c, and their printed outputs were checked against the prose); tables and animations are machine output and were read, not re-run; No other defect found. Status raised to self-checked; verified needs a textbook or human check.

last verified: never