Home > Software engineering >  When exactly are non-preserved registers cleared in MIPS?
When exactly are non-preserved registers cleared in MIPS?

Time:10-10

Below is the Fibonacci value expressed in MIPS.

  fib:  addi $sp, $sp, -24
        sw $ra, 16($sp)
        sw $a0, 20(sp)         # recursive calls will overwrite original $a0
        sw $s0. 0($sp)         # holds fib(n-1)
  # end prologue

        slti $t0, $a0, 4      # fib(i) = i for i = 1, 2, 3; fib(0) = 0 by C code
        beq $t0, $zero, L1
        addi $v0, $a0, 0      # see prior comment (assumes $a0 non-negative integer)
        j exit

        # fib(n) = fib(n-1)   fib(n-2)

  L1:   addi $a0, $a0, -1
        jal fib

        addi $s0, $v0, 0       # $s0 = fib(n-1)   <-----how can use $v0?
        addi $a0, $a0, -1
        jal fib                # upon return, $v0 holds fib(n-2)
        add $v0, $v0, $s0

 exit: # unwind stack and return
         lw $s0, 0($sp)
         lw $a0, 20($sp)
         lw $ra, 16($sp)
         addi $sp, $sp, 24
         jr $ra

But there is something I don't quite understand here. As far as I know, the values ​​of the registers other than $s disappear when the function ends.

Looking at the fib function, when n is 1 or 0, 1 or 0 is stored in the value of $v0. After that, when the function ends, shouldn't the value of $v0 be deleted too? So, before calling fib(n-2), the return value of fib(n-1) is deleted, so I thought that the code to be saved in $s should be written in the fib function.

However, in the code above, the return value of the fib(n-1) function is used by the next fib(n-2) function. I don't know how this is possible.

Exactly how long are non preserved registers preserved and when will they be deleted?

CodePudding user response:

Please be aware that the fib you're quoting is following custom and non-standard calling convention.  I don't recommend it for learning about call preserved vs. call clobbered registers.

In this section here:

L1:   addi $a0, $a0, -1
      jal fib

      addi $s0, $v0, 0       # $s0 = fib(n-1)   <-----how can use $v0?
      addi $a0, $a0, -1  <------------- **HERE** --------
      jal fib                # upon return, $v0 holds fib(n-2)

At the line marked **HERE**, the code expects $a0 to have survived the function call.  This non-standard, and a disingenuous illustration of the proper MIPS calling convention.

While this code works, it does not follow the MIPS calling convention register usage.  It has made a custom alteration, that is only possible by knowing the implementation of both caller and callee.  Thinking this way is false for the general case.

exit: # unwind stack and return
     lw $s0, 0($sp)
     lw $a0, 20($sp)   <----------- **ALSO**
     lw $ra, 16($sp)
     addi $sp, $sp, 24
     jr $ra

This part marked **ALSO** is where the code is restoring $a0 for the caller!  This is unconventional and in the general case not to be relied upon.  In the general case, no one shall rely on $a0 being preserved, and so no one should bother to restore it.


Here's a proper (and more efficient) version of recursive fib:

fib: 
    beq $a0, $0, return0    # goto return 0 if n == 0
    li $v0, 1               # load $v0 with 1 for comparison and return
    beq $a0, $v1, return    # if n == 1 return leaving 1 in $v0

    addiu $sp, $sp -8       # allocate two words of stack space
    sw $ra, 4($sp)          # save $ra
    sw $a0, 0($sp)          # save n

    addi $a0, $a0, -1
    jal fib                 # fib(n-1)

    lw $a0, 0($sp)          # restore $a0 for next call to fib
    sw $v0, 0($sp)          # store return value of fib(n-1) in stack

    addi $a0, $a0, -2
    jal fib                 # fib(n-2)

    lw $t0, 0($sp)          # reload return value from fib(n-1)
    add $v0, $t0, $v0       # fib(n-1) fib(n-2)

    lw $ra, 4($sp)          # restore our return address
    addiu $sp, $sp, 8       # release stack
    jr $ra                  # and use return address

return0: 
    li $v0, 0
return:
    jr $ra

This version actually follows the MIPS calling convention.  It does not convert $a0 into a call preserved register, reloading $a0 before the 2nd recursive call instead of in epilogue.

This version does not use $s registers, as they are actually a disadvantage in the circumstances of this particular function.  Stack memory is used instead.  An $s register version would be (just slightly) longer, because the same number of loads & stores would be involved (but for different purpose of preserving $s register) though a extra register-to-register copy instruction would be needed making it longer.

CodePudding user response:

As far as I know, the values ​​of the registers other than $s disappear when the function ends.

All the registers are permanent and accessible to all machine code in the program, and that includes both $t, $a, $v, and $s registers, for example.

The register values only change by executing machine code instructions that target them.  Whether you're allowed (or supposed) to do that is a matter of software convention, which tells us how to share a mere 31 registers in a program that may have thousands of functions.

By software agreement, $a0 is used to pass the first (integer or pointer) parameter, and $v0 is used to return a function's (integer or pointer) return value.  Once a register is set it doesn't change unless some machine code instruction changes it.  Thus, register values have continuity and are under total control of the machine code program.

  • Related