Course Homepage

Functions

Code generation overview

What are functions in programming languages?

Function abstraction: encapsulate a computation

Function behavior

(Diagram)

Function calls in assembly

A call to function f():

retval = f()

A simple definition of a function f():

begin f()
return 5
end

x86-64 assembly

The assembly call is a branch to label f. The function definition:

f:
  mov $5, %rax
  ret

This is the call to function f():

call f
mov %rax, -16(%rbp)

The return value is saved to register %rax by convention, which is saved to a variable via a mov.

-16(%rbp) represents the retval variable. We’ll see how this works below.

How call and ret work

Example

   f:
1:    mov $5, %rax
2:    ret
   _start:
3:    call f
4:    mov %rax, -16(%rbp)

_start is the entry point to the program (think main).

Diagram

What if we have multiple nested function calls?

Use a stack

We need to save multiple return addresses while we wait for returns

Multiple calls

   h:
1:    mov $7, %rax
2:    ret
   g:
3:    call h
4:    ret
   _start:
5:    call g
6:    mov %rax, -16(%rbp)

Diagram

Function-local variables

int saved_n;  // One variable for all calls to f()

int f(int n) {
  int f_n;

  if (n <= 1) {
    return 1;
  } else {
    saved_n = n;
    n = n - 1;
    f_n = f(n);
    return f_n * saved_n;
  }
}

int main() {
  f(3);
}

Diagram

We need saved_n to be function-local

We need a fresh memory location for each call to a function, i.e., each call to f.

If instead we have one memory location for each definition, recursion will not work as expected.

Factorial with function-local variables

int f(int n) {
  int saved_n;  // One variable for each call to f()
  int f_n;

  if (n <= 1) {
    return 1;
  } else {
    saved_n = n;
    n = n - 1;
    f_n = f(n);
    return f_n * saved_n;
  }
}

int main() {
  f(3);
}

How do we store function-local variables? Use a stack

Diagram

Local variable allocation

begin example_variables()
locals x, y
x = 1
y = x
return y
end

x86-64 stack operations

The stack grows downwards

Implementation of push and pop - push %rax is equivalent to - sub $8, %rsp - mov %rax, (%rsp) - pop %rax - mov (%rsp), %rax - add $8, %rsp

Technically there are multiple push operations depending on the type and size of the operand

Allocating space for local variables

Slang

begin example_variables()
locals x, y
# ...
return y
end

Assembly:

example_variables:
  sub $16, %rsp # Allocate stack space for locals
  # ...
  add $16, %rsp # Deallocate stack spcae for locals
  ret

Diagram

Using stack space for local variables

Diagram

Allocating local variables

Why not use the stack pointer, %rsp, instead?

It may be used within the function to store data, making it hard to compute the offset

Setting the base pointer

example_variables:
  mov %rsp, %rbp    # Set the base pointer first
  sub $16, %rsp     # Allocate stack space for locals
  # ...
  add $16, %rsp     # Remove local variable stack space
  ret

set the base pointer before allocating stack space so that we always have the same offset from rbp for each variable.

The symbol table

Record the offset from %rbp (the base pointer)

locals x, y
Variable Offset
x -16
y -24

Why negative offsets? Because the stack grows downwards addresses, so we store the beginning of the locals and push space for all of them.

Why start from -16? Don’t want to overwrite what’s already at %rbp (which is actually the return address since we haven’t saved the old rbp yet).

Diagram

Complete local variable example

Caller

retval = example_variables()

Callee

begin example_variables()
locals x, y
x = 1
y = x
return y
end

Recall

example_variables

Symbol table for example_variables

Variable Offset Assembly operand
x -16 -16(%rbp)
y -24 -24(%rbp)

Assembly

example_variables:
  # Set the base pointer
  mov %rsp, %rbp

  push %rbx
  # Allocate stack space for locals
  sub $16, %rsp

  # x = 1
  mov $1, %rax
  mov %rax, -16(%rbp)

  # y = x
  mov -16(%rbp), %rax
  mov %rax, -24(%rbp)

  # Return value
  mov -24(%rbp), %rax

  # Remove local variable stack space
  add $16, %rsp
  pop %rbx
  ret

Diagram

Saving the caller’s base pointer

Where does rbp point to after a function returns?

It still points to the callee’s stack frame

Save and update rbp

Save the caller’s rbp before setting it

push %rbp       # Save old base ponter
mov %rsp, %rbp  # Set new base pointer

Restore rbp

Pop the caller’s rbp before returning

pop %rbp    # Restore the caller's base pointer

Complete stack frame setup

example_variables:
  push %rbp         # Save the caller's base pointer
  mov %rsp, %rbp    # Set the base pointer

  push %rbx         # Save old %rbx

  sub $16, %rsp     # Allocate stack space for locals

  # ...

  add $16, %rsp     # Remove local variable stack space
  pop %rbx          # Restore the caller's base pointer
  pop %rbp          # Restore the caller's base pointer

  ret               # Return

Function prologue

push %rbp       # Save the caller's base pointer
mov %rsp, %rbp  # Set the base pointer
push %rbx       # Save old %rbx
sub $16, %rsp   # Allocate stack space for locals

Function epilogue

add $16, %rsp   # Remove local variable stack space
pop %rbx        # Restore old %rbx
pop %rbp        # Restore the caller's base pointer
ret

Complete example

Caller

begin main()
locals retval
retval = example_variables()
return retval
end

Callee

begin example_variables()
locals x, y
x = 1
y = x
return y
end

Assembly

example_variables:
  # Prologue
  push %rbp         # Save the caller's base pointer
  mov %rsp, %rbp    # Set the base pointer
  push %rbx         # Save old %rbx
  sub $16, %rsp     # Allocate stack space for locals

  # x = 1
  mov $1, %rax
  mov %rax, -8(%rbp)

  # y = x
  mov -8(%rbp), %rax
  mov %rax, -16(%rbp)

  # return y
  mov -16(%rbp), %rax

  # Epilogue
  add $16, %rsp     # Remove local variable stack space
  pop %rbx          # Restore old %rbx
  pop %rbp          # Restore the caller's base pointer
  ret

Diagram

Calling convention

More resources on the ABI and calling conventions

https://sourceware.html/git/?p=glibc.git;a=blob;f=stdio-common/vfprintf-internal.c;h=547a3a868b4668bf615cf3f39a92e3c11cbb98ad;hb=HEAD#l1288

https://wiki.osdev.html/Calling_Conventions

https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/

https://en.wikipedia.html/wiki/X86_calling_conventions#Register_preservation

https://stackoverflow.com/questions/1658294/whats-the-purpose-of-the-lea-instruction

https://www.fireeye.com/blog/threat-research/2008/03/instruction-poi.html

Layout

Caller has access to the parameters and its own return address.

The callee also has to save certain registers if it uses them, e.g., rbx

Passing parameters

Since each function has its own local state, how do we communicate values from one function to another?

Registers and stack parameters

Complete stack frame

Stack contents Managed by
Parameter N Caller
Parameter N-1 Caller
Caller
Parameter 8 Caller
Parameter 7 Caller
Return address Caller
———————– ————
Old base pointer Callee
Local variable 1 Callee
Local variable 2 Callee
Local variable N Callee

Where are parameters 1-6? These are the parameters passed via registers in the x86-64 System V ABI

The stack frame layout on x86_64.

See this blog post for more information.

Parameter example

begin main()
locals retval, x
x = 3
retval = func(x)
return retval
end
begin func(a)
return a
end

Assembly

main:
  # Prologue
  pushq %rbp        # Save old base ponter
  movq %rsp, %rbp   # Set new base pointer
  push %rbx         # Save old %rbx
  sub $16, %rsp     # Allocate stack space for locals

  # Assign 3 to x
  mov $3, %rax
  mov %rax, -24(%rbp)

  # Function call
  mov -24(%rbp), %rdi
  call func
  mov %rax, -16(%rbp)

  # Set return value
  mov -16(%rbp), %rax

  # Epilogue
  add $16, %rsp     # Deallocate stack space for locals
  pop %rbx          # Restore old %rbx
  pop %rbp          # Restore %rbp
  ret               # Return

func:
  # Prologue
  pushq %rbp        # Save old base ponter
  movq %rsp, %rbp   # Set new base pointer
  push %rbx         # Save old %rbx
  sub $8, %rsp      # Allocate stack space for locals

  # Move register parameter a to local variable
  mov %rdi, -16(%rbp)

  # Set return value
  mov -16(%rbp), %rax

  # Epilogue
  add $8, %rsp      # Deallocate stack space for locals
  pop %rbx          # Restore old %rbx
  pop %rbp          # Restore %rbp
  ret               # Return