SEN2201 · Week 12 · Programming in C

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.

functionprototypeargumentparametercall by valuereturn valuestatic local variableruntime stackactivation record (stack frame)frame pointer (R5)stack pointer (R6)global pointer (R4)dynamic linkreturn addressrecursionbase case

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 void return 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 variableWhere does it live (LC-3)?Lifetime
Localon the stack, in the activation recordas long as the function runs
Globalin the global data region (R4)the whole program
static localin the global data regionthe 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:

RegisterJob
R6stack pointer: the top of the stack
R5frame pointer: the record of the running function (the first local variable)
R4global pointer: the region of global variables
R7the 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:

  1. Push the arguments onto the stack from right to left (so the 1st argument is closest to the top).
  2. JSR function.
  3. On return, read the return value at the top, and discard the return value and the arguments from the stack.

The callee:

  1. Make room for the return value (R6 - 1).
  2. Push R7 (the return address).
  3. Push R5 (the dynamic link).
  4. R5 ← R6 - 1 (the new frame), make room for local variables.
  5. Run the body; access locals with LDR/STR Rx, R5, #-k, arguments with LDR Rx, R5, #4+k.
  6. 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:

  1. The base case: a small case answered without recursion.
  2. 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.

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 ideaThe truth
swap(x, y) swaps x and yThe copies change; pointers are needed
A static local is zeroed on every callInitialized once, keeps its value
The stack grows up (toward larger addresses)In the LC-3 it grows toward lower addresses
R6 is the frame pointerR6 is the stack pointer, R5 is the frame pointer
The first argument is at R5 + 3R5 + 3 is the return value; the first argument is R5 + 4
Arguments are pushed from left to rightFrom right to left; the 1st argument is near the top
The printf after the recursive call is in the same orderIn reverse order (as the stack unwinds)
It ends even without a base caseInfinite recursion, stack overflow

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

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
1 2

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).

Question 2easy

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
5 11

f works with a copy of a: x = 10, it returns 11. a is still 5.

Question 3medium

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
3 2

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.

Question 4easy

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
120

fact(5) = 5 · fact(4) = 5 · 4 · fact(3) = … = 5 · 4 · 3 · 2 · 1 = 120. The base case is n ≤ 1.

Question 5medium

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
3 2 1 1 2 3

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).

Question 6hard

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
5, 15 calls

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.

Question 7medium

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
5

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.

Question 8easy

In the LC-3 C calling convention, which registers are the stack pointer, the frame pointer, and the global data pointer?

Show answer
R6 stack pointer, R5 frame pointer, R4 global pointer

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.

Question 9medium

List what an LC-3 activation record contains.

Show answer
Local variables, dynamic link, return address, return value, arguments

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.

Question 10hard

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
a at x3FFE, b at x3FFF; x3FFB holds the caller's R5 (dynamic link)

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.

Question 11medium

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
16 81

dsum(4705) = 5 + dsum(470) = 5 + 0 + dsum(47) = 5 + 0 + 7 + 4 = 16. power(3, 4) = 3 · 3 · 3 · 3 · 1 = 81.

Question 12hard

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
321 123

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).

Review card

Call by value: parameter = COPY. swap(int,int) does not work -> pointers are needed return returns a single value | a void function returns no value static local: initialized once, keeps its value between calls (not on the stack) Runtime stack: LIFO, grows toward lower addresses in the LC-3 R6 = stack pointer (top) | R5 = frame pointer (first local) | R4 = global pointer | R7 = return address Activation record (relative to R5): local R5+0,-1.. | dynamic link R5+1 | return addr R5+2 | return value R5+3 | arg1 R5+4, arg2 R5+5 Caller: push the arguments from RIGHT TO LEFT, JSR, take the value on return, pop Callee: room for the return value, push R7, push R5, R5=R6-1, allocate locals ... restore, RET Recursion: base case + call itself with a reduced problem print before/after: before -> on the way down (in order), after -> on the way up (reverse order) fib(n) number of calls: 1,1,3,5,9,15 (n=0..5) | fact(n) depth n (+ main)