CS 202 - Notes 2018-10-24

x86-64 Assembly: Procedures

Basic concerns

In order to be able to implement procedures in assembly, we need to be able to support the following things.

  • Data management
    • need to be able to pass arguments
    • Need to return a value
  • Control flow
    • Need to be able to jump to the procedure
    • Need to be able to return to the instruction immediately after the call when we are done
  • Memory management
    • Need to be able to create local variables
    • Need local variables to go away when we are done (talk to someone who knows some JavaScript for some more complicated forms of this)
    • Local variables of the caller need to be preserved
    • Functions must be re-entrant (we need to be able to enter a second time before the first invocation is complete -- see recursion)

The stack is at the heart of how we address most of these needs

If a function needs to save a value, it pushes it onto the stack. Memory access can then be made relative to the stack pointer. Since the function is active, everything it is concerned about is at the top of the stack. We refer to the block of memory the function is in control of through the stack as its stack frame.

Control flow

call *label* : To jump to a function called label, we use this instruction. It pushes the current contents of the program counter onto the stack and then jumps to the instruction at label.

ret : Pops the value from the top of the stack into the program counter.

These two instructions allow us to call a function and then return to the exact position in the code where the call was made. The assumption, of course, is that the return address is at the top of the stack when ret is issued.

Data management

Arguments are passed in registers. You will see that our diagram of the registers lists which register corresponds to which argument.

If we need more than six arguments, then we resort to storing the arguments on the stack. The caller of a function needs to push these arguments onto the stack before issuing call. The arguments are pushed on in reverse order so that the callee knows where to find them. Thus, argument 7 should be at %rsp + 8 (one quad word before the return address), argument 8 at %rsp + 16, etc...

The return value of a function is expected to be found in %rax.

Memory management

Local variables are stored either in memory (on the stack) or in a register.

The registers are preferred, because they are faster, but we have a limited number of them, and they can't store structures (like strings or arrays). We also need to use memory if we ever dereference the variable using &, because we need a real address for it.

Local variables that are stored on the stack get popped off at the end of the function. This provides some of the variable scope -- the local variables of a function are just not available any more. This is why we should never return the address of a string or array that was not dynamically allocated from a function.

One other problem of using registers for local variables is that if we call another function, we don't want the values we stored in the registers to all be changed when the function returns. You will see that some registers are listed as being "caller saved" and some registers are listed as "callee saved". This refers to who has the responsibility for making sure that registers have the same value across a function call.

If we are writing a function, we can use any register marked "caller saved". The assumption being that if there was anything important in them whichever function called our function saved them before making the call. On the other hand, we have to assume that anything in a "callee saved" register is important and we need to save it before we use it and restore it back to the original state when we are done.

Stack discipline

Stack discipline refers to our standard practices in manipulating the stack so that it doesn't get corrupt. As an example, imagine we have two functions F1 and F2. F1 would like to call F2.

  • F1 pushes any "caller saved" registers that it wants to save onto the stack
  • F1 loads argument 1-6 into the appropriate registers
  • F1 pushes any additional arguments onto the stack in reverse order
  • F1 calls F2, which pushes the PC onto the stack
    • F2 pushes any "callee saved" registers it needs to use onto the stack
    • F2 pushes local variables onto the stack.
    • F2 executes its functionality, knowing how to access its local variables and arguments greater than 6 relative to the top of the stack (which is stored in %rsp)
    • The result of the function is placed in %rax
    • F2 pops off the local variables. Since these don't need to be stored anywhere, we frequent do this by just adding a value to %rsp, effectively moving the stack pointer up over the local variables.
    • F2 pops any "callee saved" values back into their original registers
    • F2 issues ret, which pops the return address off the stack into the program counter
  • F1 pops off any arguments greater than 6 to clean up the stack
  • F1 pops any "caller saved" values back into their original registers
  • F1 makes use of the return value stored in %rax

Note that all stack operations are balanced -- any push is balanced by a pop. Following this discipline means that functions have the appearance of doing nothing more than placing a value in %rax and the stack is always in a valid state.

Last Updated: 10/25/2018, 1:47:28 PM