CD Notes
Complete guide to activation records, stack frames, and function call mechanics
Introduction to Activation Records
Every time you call a function, something magical happens behind the scenes. The computer needs to remember where it was before the call, save the function's parameters, create space for local variables, and eventually find its way back. The data structure that manages all of this is called an activation record (also known as a stack frame). Think of it as a "work ticket" — every function call gets its own ticket containing everything needed to perform that function's work and return the result.
Understanding activation records is fundamental to compiler design because the compiler is responsible for generating all the code that creates, manages, and destroys these records. Every function prologue, epilogue, and calling convention is ultimately about managing activation records correctly.
Structure of an Activation Record
An activation record contains several pieces of information, organized in a specific order determined by the platform's calling convention:
Each component serves a specific purpose. Let us examine them one by one.
Components Explained
Return Address
When function A calls function B, the CPU must remember where to continue executing A after B finishes. The CALL instruction automatically pushes the address of the next instruction onto the stack. When B executes RET, it pops this address and jumps back.
Saved Frame Pointer
The frame pointer (RBP on x86-64) provides a stable reference point within the current activation record. By saving the previous RBP and establishing a new one, we create a linked list of stack frames that can be walked for debugging (stack traces):
Local Variables
The compiler allocates space for all local variables at known offsets from the frame pointer. For example, in void f() { int x; int y; double z; }, the compiler might assign:
xat[RBP - 4]yat[RBP - 8]zat[RBP - 16]
Temporary Storage
During expression evaluation, intermediate results need somewhere to live. For a * b + c * d, the compiler might store a * b in a temporary slot while computing c * d.
Function Call Sequence: Step by Step
Let us trace exactly what happens when main() calls add(3, 5):
int add(int a, int b) {
int result = a + b;
return result;
}
int main() {
int x = add(3, 5);
printf("%d\n", x);
}Step 1: Caller prepares arguments (in main)
MOV EDI, 3 ; First argument in EDI register
MOV ESI, 5 ; Second argument in ESI registerStep 2: CALL instruction executes
CALL add ; Push return address, jump to addStep 3: Callee prologue (beginning of add)
PUSH RBP ; Save main's frame pointer
MOV RBP, RSP ; Establish new frame pointer
SUB RSP, 16 ; Allocate space for locals (result + alignment)Step 4: Function body executes
Step 5: Callee epilogue (end of add)
Step 6: Caller receives result (back in main)
The Call Stack in Action
When functions call other functions, activation records stack up. Let us visualize the stack during a recursive call to factorial(3):
| │ return addr | fact(2) │ |
| │ return addr | fact(3) │ |
| │ return addr | main │ |
| │ return addr | OS │ |
| factorial(1) returns 1 | its frame is popped |
| factorial(2) computes 2*1=2 | its frame is popped |
| factorial(3) computes 3*2=6 | its frame is popped |
Compiler's Decisions About Activation Records
The compiler makes several important decisions during compilation:
Frame size calculation: The compiler sums up all local variable sizes, adds alignment padding, and determines the total stack allocation needed in the prologue.
Variable offset assignment: Each local variable gets a fixed offset from RBP. The compiler tracks these in its symbol table and uses them to generate memory access instructions.
Register allocation vs stack storage: Frequently used variables might be kept in registers rather than stack slots, improving performance. The compiler's register allocator makes this decision.
Calling convention compliance: The compiler must follow the platform's calling convention (which registers pass arguments, which are callee-saved, where the return value goes).
Activation Records for Nested Functions
Some languages (Pascal, Ada, modern JavaScript closures) support nested functions that can access variables from enclosing scopes. This requires an additional pointer in the activation record called the static link or access link:
procedure outer;
var x: integer;
procedure inner;
begin
x := x + 1; (* inner accesses outer's x *)
end;
begin
x := 5;
inner; (* inner's AR has static link pointing to outer's AR *)
end;The static link points to the activation record of the lexically enclosing function, allowing inner to find x by following the link and accessing the appropriate offset.
Display Table Optimization
Following static links through multiple levels (for deeply nested functions) is slow. An alternative is the display — an array where display[i] directly points to the activation record of the function at nesting depth i. This provides O(1) access to any enclosing scope's variables.
Debugging with Activation Records
When a program crashes and you see a "stack trace," the debugger is walking the chain of activation records by following saved frame pointers:
| factorial() at factorial.c | 3 |
| factorial() at factorial.c | 5 |
| factorial() at factorial.c | 5 |
| main() at main.c | 8 |
Each line represents one activation record on the stack. The debugger reads the saved RBP to find the previous frame, reads the return address to determine the calling location, and uses debug information to map addresses to source lines.
Key Takeaways
Activation records are the fundamental mechanism enabling function calls, recursion, and local variable scoping in compiled programs. The compiler generates prologue code to create each record and epilogue code to destroy it. Every variable access, function call, and return statement in your source code translates to operations on activation records. Understanding this mechanism explains why recursion works (each call gets independent storage), why local variables are fast (fixed offsets from frame pointer), and why stack overflows happen (too many records exceed available stack space).
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Activation Records: Function Call Management.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Compiler Design topic.
Search Terms
compiler-design, compiler design, compiler, design, runtime, environment, activation, records
Related Compiler Design Topics