Search code examples
assemblymipssubroutinemars-simulator

Subroutines with nested calls in MIPS


This program is meant to replace all the lowercase letters in a string with the character *.

The problem I am having is in the nested call of subroutines. I.e. some same $t and $a registers are being used in different subroutines. So, when a subroutine is called in another subroutine, the caller subroutine's registers gets messed up.

.data
    str: .asciiz "WindOnTheHill" 

.text
    la $a0, str # start of the string
    li $a1, '*'
    jal ReplaceAllLower

    #la $a0, str # start of the string
    jal PrintStr
    jal Exit

ReplaceAllLower:
    # backup return address
    addi $sp, $sp, -12 #  create space for 3 words
               # (3*4=12 bytes) on the stack 
               # (push) for $ra
    sw $ra, 0($sp) # backup return address $ra

    # protect arguments from change
    sw $a0, 4($sp) # backup string address
    sw $a1, 8($sp) # backup char 

    # get string length
    jal StrLen # obtain string length
    move $t0, $v0 # backup string length

    # retrieve argument values  
    lw $a1, 8($sp) # restore char 
    lw $a0, 4($sp) # restore string address

    move $t1, $a0 # obtain string address
    move $t2, $a1 # obtain char
    li $t3, 0 # loop counter    

    while:  
        bgt $t3, $t0, end_while 

        jal IsLower

        beq $t0, 1, lower_case
        j not_lower_case

        lower_case:
            sb $t2, ($a0)           

        not_lower_case: 
            addi $a0, $a0, 1 # increment address
            addi $t3, $t3, 1 # increment loop counter

        j while
    end_while:  

    move $a0, $t1

    # restore stack     
    lw $ra, 0($sp) # restore $ra
    addi $sp, $sp, 16 # return the space on the stack(pop)

    # return 
    jr $ra  

IsLower:
    lb $t0, ($a0) # obtain the character
    li $t1, 97 # 'a' - character
    li $t2, 122 # 'z' - character

    bge $t0, $t1, con1_fulfilled #bigger tha or equal to 0  
    j con1_not_fulfilled

con1_fulfilled:
    ble $t0, $t2, con2_fullfilled #less than or equal to 9
    j con2_not_fulfilled

con2_fullfilled:
    li $v0, 1
    j return

con1_not_fulfilled:
con2_not_fulfilled:
    li $v0, 0

return:                                                                     
    # return 
    jr $ra 

StrLen:
    move $a1, $a0 # start of string
    # run a loop
    li $t0, '\0' # null character 
    li $t1, 0 # prepare the counter

    start_loop: 
        lb $v0, ($a0) # obtain the 1st character

        beq $v0, $t0, end_loop  # exit loop if '\0'-char found

        addi $t1, $t1, 1 # increment counter
        addi $a0, $a0, 1 # increment address

        j start_loop # iterate again
    end_loop:

    move $a0, $a1 #restore string address
    move $v0, $t1 # return value

    # return 
    jr $ra  

PrintStr:   
    li $v0, 4
    syscall
    # return 
    jr $ra


Exit:
    # push $s0 on stack
    addi $sp, $sp, -4 # create 4-bytes on the stack
    sw $s0, ($sp) # cpy $s0 to stack

    #terminate program
    li $v0, 10
    syscall

    # free stack  
        addi $sp, $sp, 4 
    # return 
    jr $ra  

Note: Let us not concentrate on the algorithm at this moment.

So, my question is,
What technique should I use to get rid of this issue, as it is very hard to know beforehand which subroutine is going to be called in which in future (a library can expand itself over time)?


Solution

  • Some conventions are required in such a way that if all subroutines respect them, there will not be any problem such as a register being clobbered by a called procedure.

    For the MIPS, the generally accepted calling conventions are that:
    * registers $t0-7 are "temporary" and can be used without precautions. If a procedure wants to preserve some of them across function calls, it is its responsibility to save them ("caller saved").
    * registers $s0-7 ("saved registers") cannot be used without precaution. If a procedure wants to use some of them, it must preserve them before use and restore their value upon return ("callee saved")

    There are other important aspects in the calling conventions, such as passing the first arguments in registers $a0-$a3, using $v0-$v1 for the return value, etc. They also precise the role of some registers such a the stack pointer (sp) or the frame pointer (fp). this document is a very good summary, but you can easily find extra details on internet.

    Preserving registers is done with a call stack. It is a data structure that holds all preserved information.

    At the start of the function, some space must be reserved in the stack for all information that need to be preserved. Then registers s0-s7 that are going to be used are saved in the stack. If the function is non terminal (ie calls another function), the returned address is also saved.

    Before calling a function, temporary or argument registers ($t0-7 or $a0-3) that need to be saved are written to the stack. Arguments are written to registers $a0-3 or stacked if required. And the function is called.

    After the called function return, preserved temporary registers are restored.

    And before the function returns, one needs to restore saved $s0-7 registers and the return address register ($ra), stack space is freed and one calls jr $ra.

    If all procedures respect these calling conventions, there will not be any problem. Compilers respect these conventions, but they are dependent on OS and architectures.