In this lab you will learn how to write nested procedures and how to preserve register contents across procedure calls.
############################ # Simple procedure call ############################ .text .globl __start __start: #Do some stuff jal subProc1 #convert value to fixed point #Do more stuff li $v0, 10 #exit program syscall ############################ #subProc1 ############################ #Simple procedure ############################ #Input: None #Output: Prints a message on the console ############################ .text subProc1: la $a0, s1out1 #print a message li $v0, 4 syscall jr $ra #return from procedure .data s1out1: .asciiz "***Hello from subProc1***\n"Simple, but that doesn't work for procedures that need to call other procedures. For example this program:
############################ # Functions calling Functions # # A demo program showing how to do robust function calls using the stack. ############################ #Main program .text .globl __start __start: la $a0, out1 #print a message li $v0, 4 syscall jal subProc1 #call a simple procedure la $a0, bye #print a good bye message li $v0, 4 syscall li $v0, 10 #exit program syscall .data out1: .asciiz "Welcome to main. I will now call some procedures...\n" bye: .asciiz "Goodbye. Come again soon.\n" ############################ #subProc1 ############################ #Simple procedure ############################ #Input: None #Output: Prints a message on the console ############################ .data s1out1: .asciiz "***Hello from subProc1***\n" .text subProc1: jal subProc2 #call subprogram la $a0, s1out1 #print a message li $v0, 4 syscall jr $ra #return to caller ############################ #subProc2 ############################ #Simple procedure ############################ #Input: None #Output: Prints a message on the console ############################ .data s2out1: .asciiz "***Hello from subProc2***\n" .text subProc2: la $a0, s2out1 #print a message li $v0, 4 syscall jr $ra #return to callerWhich you might expect to produce this output:
Welcome to main. I will now call some procedures... ***Hello from subProc2*** ***Hello from subProc1*** Goodbye. Come again soon.instead it gets stuck in an endless loop and writes
***Hello from subProc1*** ***Hello from subProc1*** ***Hello from subProc1*** ***Hello from subProc1*** ***Hello from subProc1***Until you force it to quit. Why?
The answer is in the jal and jr $ra instructions. jal automatically stores the next instruction's address in $ra. When jr $ra is called it puts that value in the program counter and the program picks up with the instruction after the procedure call. However, when a procedure calls a procedure the same register, $ra, is used. If the value is not saved somewhere else and restored before returning then jr $ra will just go back to the last address stored in $ra. Execution will resume after the last function call and you have a loop.
The solution is to save $ra somewhere. If you know the procedure you are calling doesn't use a register you also are not using you could save $ra there. The problem with that is you will eventually run out of registers. You need a storage space that can get as big as you need and that is easily accessible. The stack is such a space. You should push and pop $ra at the beginning and end of every procedure that needs to call another so it always remembers where it came from.
$ra isn't the only register that needs saving. When you call a procedure you expect it to do its job and leave the registers you were using alone so that you can finish your work. Certain registers are always preserved by convention. Responsibility for saving registers is divided between the caller and the callee.
Caller-saved registers: $v0-$v1, $t0-$t9
Callee-saved registers: $ra, $a0-$a3, $s0-$s7, $gp, $sp, $fp
Bold registers must must have the same value before and after a procedure is called. If you need to keep the values in any of the other registers your code should save them before calling a procedure just in case it needs them too.
There are four tasks that should be done to make a safe procedure call:
Lets look at the tasks in detail:
For convenience you can use these two macros: push_and_allocate and pop_and_deallocate. Download them here (stackOps.m4). They will be used in the following example.
Macro | Description |
push_and_allocate |
Output: addi $sp, -16 sw $t1, $sp + 12 sw $t2, $sp + 8 sw $v0, $sp + 4 |
pop_and_deallocate |
Output: sw $t1, $sp + 12 sw $t2, $sp + 8 sw $v0, $sp + 4 addi $sp, 16 |
Note: When you use the stack, words must be aligned on a 4 byte boundary. That means you should allocate stack space for variables and arguments in multiples of 4 bytes even if you don't need the last few bytes.
One form of procedure calls itself over and over again to solve a problem. This is called a recursive procedure. This kind of procedure absolutely must follow safe procedure writing rules. The following is an example of such a procedure. You can download it here: recurse.m4. It requires these macro libraries for preprocessing: printLib.m4, stackOps.m4, foreach.m4.
Code | Nesting Role |
---|---|
############################ # # recurse.[m4|s] # # A recursive string decoder. # Decodes strings where the first part of the message is after a '~' # and the second is in reverse before the '~' # # Written to demonstrate the following MIPS procedure tricks: # - saving caller and callee registers # - passing arguments on the stack # - creating local variables on the stack # - nested functions # ############################ include(printLib.m4) include(stackOps.m4) ############################ #decode ############################ #decodes messages encoded in reverse.forward form. ############################ #Input: Argument 1(stack) - memory address pointing to a location in a # string #Output: Prints the decoded message on the console # #Algorithm: # examine character c. # case c == '~' # print everything after c # case c == end of string # break # default (c is a reversed character) # call decode with next character on string # print c # break # return from procedure # #Variable map # d_c - argument. frame pointer. Pointer to current character # d_pchr - local. frame pointer - 20. Two character string for # printing d_c ############################ .text decode: | |
#set up procedure push_and_allocate(`$ra, $fp, $s0, $a0', 4) addi $fp, $sp, 20 #set up frame pointer define(`d_pchr', `-20') dnl give a convenient name to the variable define(`d_c', `0') dnl name for argument 1. | 2. Save and Prepare to Run |
#examine character d_c lw $s0, d_c`'($fp) #get the address of the character lb $t1, ($s0) #get the character itself #begin case statement li $t2, '~' beq $t1, $t2, caseDot beq $t1, 0, caseEOL j default caseDot: addi $a0, $s0, 1 #point to next character printStr($a0) #print the rest of the string caseEOL: j endCase default: addi $s0, $s0, 1 #point to next character | |
#Set up procedure call to decode #Save caller registers push_and_allocate($t1, 4) sw $s0, d_c ($sp) #pass pointer to next character | 1. Prepare to call |
jal decode #recursively call decode | |
#clean up after procedure call #Restore caller saved registers and close stack pop_and_deallocate($t1, 4) | 4. Prepare to Continue |
#print c sb $t1, d_pchr ($fp) #put the character in our local variable li $t1, 0 #0 for null terminated string sb $t1, 1 + d_pchr ($fp)#put a null on the local variable addi $a0, $fp, d_pchr #explicit pointer to local variable printStr($a0) #print the local variable endCase: | |
#prepare to return from procedure pop_and_deallocate(`$ra, $fp, $s0, $a0',4) jr $ra | 3. Restore and Return |
############################ #Main program ############################ .data msg: .asciiz "\n!dedoced yllufsseccus~String " .text .globl __start __start: | |
#prepare to call procedure addi $sp, $sp, -4 #make space on stack for argument la $a0, msg #get address of string sw $a0, d_c ($sp) #Load argument on stack | 1. Prepare to Call |
jal decode #call procedure | |
addi $sp, $sp, 4 #close space for argument on stack | 4. Prepare to Continue |
li $v0, 10 syscall |
Things to notice:
Sometimes the rules for writing safe procedures aren't enough. There are two cases to be aware of.
|
|
|
|
|