Writing Subroutines
As illustrated before, calling of subroutines heavily hinges on a number of conventions. Thanks to those conventions, however, knowing how to call one subroutine essentially means knowing how to call any subroutine. When writing subroutines yourself it is essential to respect those conventions yourself.
Theoretically, you could make up any conventions as long as you are the only caller of your subroutines. However, following the given conventions not only helps you stay consistent but also makes your subroutines generally usable. The assignments will furthermore test whether you are following the calling conventions through the automated tests.
Callee-Saved Registers
As mentioned before, some registers are callee-saved by convention and need to retain their value during a subroutine call. However, this does not mean that you may not use them at all.
You can push the values of all callee-saved registers that you want to use to the stack at the start of the subroutine. If you pop the same values to the corresponding registers before returning it will look like the registers never changed their value to the caller of your subroutine.
Stack Frame
The stack is an area in memory that is freely useable by your subroutine. However, as by the calling convention, the stack should remain seemingly untouched for the caller of your subroutine. The area of the stack that is used by your subroutine is typically called its stack frame.
Prologue
The rbp
("base pointer") register typically denotes the bottom (→ start) of the current stack frame and does not change during the main execution of the subroutine (while the stack pointer moves with every push
or pop
instruction). To set up the stack frame of a subroutine, a so-called prologue is commonly used:
As rbp
is a callee-saved register, clearly its value at the entry to the subroutine needs to be saved somewhere such that it can be restored before returning. As explained on the page on Memory, the stack is the go-to location for temporary storage. Thereby, the previous base pointer is pushed onto the stack (line 2).
Now all that's left is to modify the base pointer (rbp
) such that it correctly points to the "base" of the new stack frame. This is done by copying the value of the stack pointer (rsp
) to the base pointer. As the stack pointer always points at the topmost element of the stack (so the element last pushed), after executing line 3 both the base pointer and the stack pointer point at the bottom of the current stack frame
The nice side-effect of this sort of prologue is concerning the stack alignment. As mentioned before, the stack should be 16-byte aligned before calling a subroutine. Calling a subroutine, however, will push the 8-byte return address. So at the start of the called subroutine, the stack will be misaligned by 8 bytes. By pushing the previous base pointer, the alignment is again restored to 16 bytes and other subroutines may be called without further action to align the stack.
Epilogue
Before a subroutine can return, the stack frame that was built in the prologue needs to be properly removed, and the previous stack frame restored. That action is called the epilogue of the function (as opposed to the prologue):
The previous base pointer is, considering a proper prologue, stored at the bottom of the current stack frame, so at the location pointed to by rbp
. Therefore, the value of the base pointer is copied to the stack pointer - the top of the stack is now again the beginning of the stack frame and any pushed values are no longer part of the stack. Next, the topmost value of the stack is popped and saved as the base pointer - the caller's value of rbp
is properly restored. Lastly, the ret
instruction will pop the return address from the stack and move it into the program counter.
Local Variables
When writing a subroutine, you will likely need some local variables. You could, of course, simply rely on registers for those variables. However, there are multiple drawbacks to that:
there is only a limited number of registers, some of which you may even need for temporary storage during computations
if your subroutine calls another subroutine, some of the registers may not keep their value
So a different place to store variables is needed. As you may have guessed the answer is the stack.
Reserving Space
To make space for local variables, you can push their initial values right after the prologue of your subroutine. If they are uninitialized, you can, even more simply, subtract the needed space from the stack pointer (remember: the stack grows towards the lower memory addresses, so if you want to reserve space for a quadword you need to subtract 8 from the stack pointer).
Accessing Local Variables
As you want to keep the variables on the stack during the execution of your subroutine, it is not a valid option to pop each variable from the stack when you need it. Rather, you want to access them somehow without modifying your stack. For that, you can use a mov
instruction with the address of the needed variable as the first operand.
As you should know, there are various Addressing Modes in x86-64 Assembly. For accessing items from the stack, it is useful to refer to them as either an offset from the stack pointer or an offset from the base pointer. The stack pointer changes its value with every push or pop, making it mostly unusable for referring to local variables for longer subroutines. The base pointer, however, remains unchanged during the execution of a subroutine (except for during the pro- and epilogue). Thereby, local variables are commonly referred to as an offset from the base pointer.
Last updated