C functions, the runtime stack and recursion
Function definition, call by value, static local variables, the activation record on the LC-3 (R5 frame pointer, R6 stack pointer) and tracing recursive functions.
Until now everything was inside main. Real programs are divided into functions: each does a single job, has a name, takes parameters, returns a value. This page explains the rules of functions in C, how they work on the LC-3 (the runtime stack) and functions that call themselves (recursion). Two kinds of questions come up on the exam: “What is printed?” and “draw the stack: where is R5, at which offset is the parameter?”.
Function definition
int square(int x) { /* return type, name, parameter list */
return x * x;
}
int main(void) {
int r = square(4); /* argument 4 */
printf("%d\n", r);
return 0;
}
- Parameter: the variable in the function definition (
x). - Argument: the value given in the call (
4). - Prototype: if the function will be defined after main, it is declared beforehand:
int square(int x);. - A
voidreturn type: returns no value.
Call by value
In C the value of the argument is copied. If the function changes its parameter, it changes only its own copy.
void swap(int a, int b) { int t = a; a = b; b = t; }
int main(void) {
int x = 1, y = 2;
swap(x, y);
printf("%d %d\n", x, y);
return 0;
}
1 2
The explanation at the LC-3 level is simple: the arguments are copied onto the stack, swap changes those copies, and when the function returns that region is discarded from the stack. For a swap you need to pass the addresses of x and y (pointers, week 13).
Scope and lifetime
| Kind of variable | Where does it live (LC-3)? | Lifetime |
|---|---|---|
| Local | on the stack, in the activation record | as long as the function runs |
| Global | in the global data region (R4) | the whole program |
| static local | in the global data region | the whole program, but seen only from inside the function |
int counter(void) { static int c = 0; c++; return c; }
The first call returns 1, the second 2, the third 3. Without static, 1 every time.
The runtime stack
For every function call, an activation record (or stack frame) is opened in memory. The records are kept in stack order: last in, first out (LIFO). When main calls a function, its record is placed on top of main’s; when the function returns, it is removed. This is why recursion works: each call has its own record, that is, its own n.
In the LC-3 C compiler:
| Register | Job |
|---|---|
| R6 | stack pointer: the top of the stack |
| R5 | frame pointer: the record of the running function (the first local variable) |
| R4 | global pointer: the region of global variables |
| R7 | the return address written by JSR |
The stack grows toward lower addresses: push = decrease R6 by one, then write.
The structure of the activation record
low address
+--------------------+
R5 - 1 → | 2nd local variable |
R5 → | 1st local variable |
R5 + 1 | dynamic link | the caller's R5
R5 + 2 | return address | the caller's R7
R5 + 3 | return value |
R5 + 4 | 1st argument | pushed by the caller
R5 + 5 | 2nd argument |
+--------------------+
high address
The calling steps
The caller:
- Push the arguments onto the stack from right to left (so the 1st argument is closest to the top).
JSR function.- On return, read the return value at the top, and discard the return value and the arguments from the stack.
The callee:
- Make room for the return value (R6 - 1).
- Push R7 (the return address).
- Push R5 (the dynamic link).
- R5 ← R6 - 1 (the new frame), make room for local variables.
- Run the body; access locals with
LDR/STR Rx, R5, #-k, arguments withLDR Rx, R5, #4+k. - Write the return value to R5 + 3, discard the locals, restore R5 and R7,
RET.
Worked example: add2(3, 9)
int add2(int a, int b) { int s; s = a + b; return s; }
/* main: r = add2(3, 9); */
In the simulator, initially R6 = R5 = x4000. Inside add2, right after the line s = a + b, the stack is:
address content relative to R5 meaning
x3FFA x000C R5 + 0 s = 12
x3FFB x4000 R5 + 1 dynamic link (main's R5)
x3FFC x300B R5 + 2 return address (the instruction after JSR)
x3FFD x0000 R5 + 3 return value (not written yet)
x3FFE x0003 R5 + 4 a = 3
x3FFF x0009 R5 + 5 b = 9
R5 = x3FFA, R6 = x3FFA
The LC-3 counterpart of the body:
LDR R0, R5, #4 ; a
LDR R1, R5, #5 ; b
ADD R0, R0, R1
STR R0, R5, #0 ; s = a + b
After the return, main reads R0 = 12 from the top, and R6 becomes x4000 again.
Recursion
A recursive function has two parts:
- The base case: a small case answered without recursion.
- The recursive step: reducing the problem and calling itself.
If there is no base case or the problem does not shrink, the stack overflows.
int fact(int n) {
if (n <= 1) return 1; /* base case */
return n * fact(n - 1); /* reduced problem */
}
fact(4)
= 4 * fact(3)
= 4 * (3 * fact(2))
= 4 * (3 * (2 * fact(1)))
= 4 * (3 * (2 * 1))
= 24
At the deepest moment there are four fact records and main’s record on the stack.
Print order: before or after?
void p(int n) {
if (n == 0) return;
printf("%d ", n); /* going down */
p(n - 1);
printf("%d ", n); /* coming back up */
}
p(3):
3 2 1 1 2 3
The rule: work before the recursive call is done in order on the way down, work after it is done in reverse order as the stack unwinds.
The call tree
For functions that call themselves twice, draw a tree:
fib(4)
/ \
fib(3) fib(2)
/ \ / \
fib(2) fib(1) fib(1) fib(0)
/ \
fib(1) fib(0)
number of nodes = 9 calls, fib(4) = 3
15 calls for fib(5). The same subproblems (like fib(2)) are computed again and again.
Common mistakes
| Wrong idea | The truth |
|---|---|
| swap(x, y) swaps x and y | The copies change; pointers are needed |
| A static local is zeroed on every call | Initialized once, keeps its value |
| The stack grows up (toward larger addresses) | In the LC-3 it grows toward lower addresses |
| R6 is the frame pointer | R6 is the stack pointer, R5 is the frame pointer |
| The first argument is at R5 + 3 | R5 + 3 is the return value; the first argument is R5 + 4 |
| Arguments are pushed from left to right | From right to left; the 1st argument is near the top |
| The printf after the recursive call is in the same order | In reverse order (as the stack unwinds) |
| It ends even without a base case | Infinite recursion, stack overflow |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
What is printed?
void swap(int a, int b) { int t = a; a = b; b = t; }
int main(void) {
int x = 1, y = 2;
swap(x, y);
printf("%d %d\n", x, y);
return 0;
}Show answer
In C arguments are passed by value (call by value): a and b are copies of x and y. swap changes its own copies and does not touch x and y. For a real swap you need pointers (week 13).
What is printed?
int f(int x) { x = x * 2; return x + 1; }
int main(void) {
int a = 5;
int b = f(a);
printf("%d %d\n", a, b);
return 0;
}Show answer
f works with a copy of a: x = 10, it returns 11. a is still 5.
What is printed?
int counter(void) { static int c = 0; c++; return c; }
int plain(void) { int c = 0; c++; return c; }
int main(void) {
counter();
counter();
printf("%d %d\n", counter(), plain() + plain());
return 0;
}Show answer
A static local variable is initialized once and keeps its value between calls (it lives not on the stack but in the global data region). The third call returns 3. The c in plain becomes 0 again on every call, each call returns 1: 1 + 1 = 2.
What is printed?
int fact(int n) {
if (n <= 1) return 1;
return n * fact(n - 1);
}
/* in main: */ printf("%d\n", fact(5));Show answer
fact(5) = 5 · fact(4) = 5 · 4 · fact(3) = … = 5 · 4 · 3 · 2 · 1 = 120. The base case is n ≤ 1.
What is printed by p(3)?
void p(int n) {
if (n == 0) return;
printf("%d ", n);
p(n - 1);
printf("%d ", n);
}Show answer
Each call first prints its own n, then makes a call one level down, and when that call finishes it prints its n again. Going down 3 2 1, p(0) returns immediately, coming back up 1 2 3. The second printfs run in reverse order as the stack unwinds (LIFO).
With the recursive fib below, what is fib(5), and how many times is fib called in total (including the first call)?
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}Show answer
The number of calls C(n) = 1 + C(n-1) + C(n-2), C(0) = C(1) = 1. C(2) = 3, C(3) = 5, C(4) = 9, C(5) = 15. The value: fib(5) = 5. The same subproblems are computed again and again; that is why this method is slow.
main calls fact(4) (the function above). What is the maximum number of activation records on the runtime stack at the same time, counting main?
Show answer
fact(4) → fact(3) → fact(2) → fact(1). While fact(1) is running, four fact records and main's record are on the stack: 5. fact(1) returns at the base case, and the records are removed in reverse order.
In the LC-3 C calling convention, which registers are the stack pointer, the frame pointer, and the global data pointer?
Show answer
R6 always points to the top of the stack. R5 points to the activation record of the function currently running (its first local variable); local variables are read with an offset from R5. R4 points to the region of global variables. R7 is used for the return address.
List what an LC-3 activation record contains.
Show answer
Local variables (R5+0, R5-1, …), the dynamic link (the caller's R5, R5+1), the return address (the caller's R7, R5+2), room for the return value (R5+3) and the arguments (from R5+4 on). The caller pushes the arguments onto the stack, the called function pushes the rest.
add2(3, 9) is called with the LC-3 convention. Inside add2, R5 = x3FFA. At which addresses are the parameters a and b, and what does x3FFB contain?
int add2(int a, int b) { int s; s = a + b; return s; }Show answer
The first parameter is at R5 + 4 = x3FFE (3), the second at R5 + 5 = x3FFF (9). Because the arguments are pushed from right to left, a is closer to the top.
x3FFB = R5 + 1: the dynamic link (x4000 in the simulation). x3FFC = R5 + 2: the return address, x3FFD = R5 + 3: the return value, x3FFA = R5 + 0: s = 12.
What is printed?
int dsum(int n) { if (n < 10) return n; return n % 10 + dsum(n / 10); }
int power(int b, int e) { if (e == 0) return 1; return b * power(b, e - 1); }
/* in main: */ printf("%d %d\n", dsum(4705), power(3, 4));Show answer
dsum(4705) = 5 + dsum(470) = 5 + 0 + dsum(47) = 5 + 0 + 7 + 4 = 16. power(3, 4) = 3 · 3 · 3 · 3 · 1 = 81.
What is printed?
void rev(int n) { if (n == 0) return; printf("%d", n % 10); rev(n / 10); }
void up(int n) { if (n == 0) return; up(n / 10); printf("%d", n % 10); }
/* in main: */ rev(123); printf(" "); up(123);Show answer
rev prints the digit FIRST, then recurses: it starts from the last digit → 321. up recurses first, prints on the way back: the deepest call prints the first digit (1) first → 123. Whether the printf comes before or after the recursive call decides the order.
Verification: The C outputs were compiled and run with Zig cc (clang, C99) (the questions are verify/SEN2201/c/w12.c, the body examples are at the start of verify/SEN2201/c/w13.c). The LC-3 activation record was run on our own simulator with assembly written by hand according to the textbook's calling convention, and a memory dump of the stack was taken (verify/SEN2201/12-stack.mjs).