The call stack

Calling functions

When the CPU runs code it fetches instructions and executes them.

Code can change the flow of control using jump instructions that change the program counter.

This is enough to implement basic control constructs such as if and while.

But what about calling functions/methods?

Simple calls

public void main() {
  foo(10, 20);
}
public void foo(int a, int b) {
  bar(a * 10, b);
}
public void bar(int a, int b) {
}
Drawn by the content build (build/call-stacks.ts) from a <call-stack> in materials/slides/ib-eng/call-stack/slides.deck 10 20 a b ret ↩ main 100 20 a b ret ↩ foo 100 20 ↩ foo 10 20 ↩ main rsp main() main() rsp main() rsp main() foo(10, 20) rsp foo(10, 20) rsp foo(10, 20) rsp foo(10, 20) bar(100, 20) rsp foo(10, 20) rsp main() rsp

The call pushes the return address, where foo jumps back to when it’s done: the next instruction in main.

Push the argument for a.

Returning from first call to foo.

Calling bar.

Local variables

public void main() {
  int x = 1;
  foo(x);
}
public void foo(int a) {
  int b = a * 2;
  b = b + 1;
}
Drawn by the content build (build/call-stacks.ts) from a <call-stack> in materials/slides/ib-eng/call-stack/slides.deck x 1 1 a ret ↩ main b 2 3 1 ↩ main 3 1 rsp main() main() rsp main() rsp main() foo(1) rsp foo(1) rsp main() rsp rsp

Return values

public void main() {
  int x = add(10, 20);
}
public int add(int a, int b) {
  int sum = a + b;
  return sum;
}
Drawn by the content build (build/call-stacks.ts) from a <call-stack> in materials/slides/ib-eng/call-stack/slides.deck 10 20 result a b ret ↩ main sum 30 30 10 20 ↩ main 30 x 30 rsp main() main() rsp main() rsp main() rsp main() add(10, 20) rsp add(10, 20) rsp main() rsp rsp

Assigning a result

public void main() {
  int x = 0;
  x = square(5);
}
public int square(int n) {
  return n * n;
}
Drawn by the content build (build/call-stacks.ts) from a <call-stack> in materials/slides/ib-eng/call-stack/slides.deck x 0 5 result n ret ↩ main 25 5 ↩ main 25 25 25 rsp main() main() rsp main() rsp main() rsp main() square(5) rsp main() rsp main() rsp rsp

Nested calls

public void main() {
  int y = square(add(1, 2));
}
public int add(int a, int b) {
  return a + b;
}
public int square(int n) {
  return n * n;
}
Drawn by the content build (build/call-stacks.ts) from a <call-stack> in materials/slides/ib-eng/call-stack/slides.deck 1 2 result a b ret ↩ main 3 1 2 ↩ main result n ret ↩ main 9 3 ↩ main y 9 rsp main() main() rsp main() rsp main() rsp main() rsp main() add(1, 2) rsp main() rsp main() square(3) rsp main() rsp rsp

Recursion

public void main() {
  int x = factorial(3);
}
public int factorial(int n) {
  if (n <= 1) {
    return 1;
  }
  return n * factorial(n - 1);
}
Drawn by the content build (build/call-stacks.ts) from a <call-stack> in materials/slides/ib-eng/call-stack/slides.deck 3 result n ret ↩ main 2 result n ret ↩ factorial 1 result n ret ↩ factorial 1 1 ↩ factorial 2 2 ↩ factorial 1 6 3 ↩ main 2 x 6 rsp main() main() rsp main() rsp main() factorial(3) rsp factorial(3) rsp factorial(3) rsp factorial(3) factorial(2) rsp factorial(2) rsp factorial(2) rsp factorial(2) factorial(1) rsp factorial(2) rsp factorial(3) rsp main() rsp rsp

Fibonacci

public int fib(int n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
}
Drawn by the content build (build/call-stacks.ts) from a <call-stack> in materials/slides/ib-eng/call-stack/slides.deck 4 result n 3 result n ret ↩ fib 2 result n ret ↩ fib 1 result n ret ↩ fib 1 1 ↩ fib 0 result n ret ↩ fib 0 0 ↩ fib 1 2 ↩ fib 1 0 1 result n ret ↩ fib 1 1 ↩ fib 2 3 ↩ fib 1 1 2 result n ret ↩ fib 1 result n ret ↩ fib 1 1 ↩ fib 0 result n ret ↩ fib 0 0 ↩ fib 1 2 ↩ fib 1 0 3 4 2 1 rsp rsp rsp fib(4) fib(4) rsp fib(4) rsp fib(4) fib(3) rsp fib(3) rsp fib(3) rsp fib(3) fib(2) rsp fib(2) rsp fib(2) rsp fib(2) fib(1) rsp fib(2) rsp fib(2) rsp fib(2) rsp fib(2) fib(0) rsp fib(2) rsp fib(3) rsp fib(3) rsp fib(3) rsp fib(3) fib(1) rsp fib(3) rsp fib(4) rsp fib(4) rsp fib(4) rsp fib(4) fib(2) rsp fib(2) rsp fib(2) rsp fib(2) fib(1) rsp fib(2) rsp fib(2) rsp fib(2) rsp fib(2) fib(0) rsp fib(2) rsp fib(4) rsp rsp

Fibonacci, iteratively

public void main() {
  int x = fib(4);
}
public int fib(int n) {
  return fibHelper(n, 0, 1);
}
public int fibHelper(int n, int a, int b) {
  if (n == 0) {
    return a;
  }
  return fibHelper(n - 1, b, a + b);
}
Drawn by the content build (build/call-stacks.ts) from a <call-stack> in materials/slides/ib-eng/call-stack/slides.deck 4 result n ret ↩ main 4 0 1 result n a b ret ↩ fib 3 1 1 result n a b ret ↩ fibHelper 2 1 2 result n a b ret ↩ fibHelper 1 2 3 result n a b ret ↩ fibHelper 0 3 5 result n a b ret ↩ fibHelper 3 0 3 5 ↩ fibHelper 3 1 2 3 ↩ fibHelper 3 3 2 1 2 ↩ fibHelper 3 3 3 1 1 ↩ fibHelper 3 3 4 0 1 ↩ fib 3 3 4 ↩ main 3 x 3 rsp main main rsp main rsp main fib rsp fib rsp fib rsp fib rsp fib rsp fib fibHelper rsp fibHelper rsp fibHelper rsp fibHelper rsp fibHelper rsp fibHelper fibHelper rsp fibHelper rsp fibHelper rsp rsp rsp fibHelper fibHelper rsp fibHelper rsp fibHelper rsp fibHelper rsp fibHelper rsp fibHelper fibHelper rsp fibHelper rsp fibHelper rsp fibHelper rsp fibHelper rsp fibHelper fibHelper rsp fibHelper rsp fibHelper rsp fibHelper rsp fibHelper rsp fib rsp main rsp rsp

Tail call optimization

public void main() {
  int x = fib(4);
}
public int fib(int n) {
  return fibHelper(n, 0, 1);
}
public int fibHelper(int n, int a, int b) {
  if (n == 0) {
    return a;
  }
  return fibHelper(n - 1, b, a + b);
}
Drawn by the content build (build/call-stacks.ts) from a <call-stack> in materials/slides/ib-eng/call-stack/slides.deck 4 result n ret ↩ main ↩ main ret ↩ main 0 1 n a b 3 1 n a b 2 2 n a b 1 2 3 n a b 0 3 5 n a b 3 0 3 5 ↩ main x 3 rsp main() main() rsp main() rsp main() fib(4) rsp fib(4) rsp fibHelper(4, 0, 1) fibHelper(3, 1, 1) fibHelper(2, 1, 2) fibHelper(1, 2, 3) fibHelper(0, 3, 5) main() rsp rsp

Tail call optimization is a way the compiler can optimize calls that occur in the tail position meaning that there is no more computation to do after they return—whatever value the callee returns will be returned by the caller.

This is a simplified model.

In particular, we are ignoring the registers.

When one function calls another, there are probably values in registers that that function is using but the new function may also need to use those same registers.

So what actually happens is the values of registers are copied to the stack as part of the function calling machinery.

The exact details are determined by a combination of things dictated by the CPU architecture and the operating system.

Some registers are saved by code a function before it calls another (caller-saved registers) and restored after the call returns and others are saved by the callee (callee-saved registers) and restored by the callee before returning.