Search code examples
architecturemipscpu-registers

Whether $ra register callee saved or caller saved in mips?


I've read that preserved registers are caller saved and non preserved registers are callee saved. But it seems to me that $ra, a preserved register, is caller saved as the caller saves the address to which it have to return. Can any one explain what I am missing?


Solution

  • I've read that preserved registers are caller saved and non preserved registers are callee saved.

    That may not be the best way to state things and may be the source of the confusion. Here is a better way:

    A function (i.e. callee) must preserve $s0-$s7, the global pointer $gp, the stack pointer $sp, and the frame pointer $fp

    All other registers may be changed by a function as it sees fit.

    For example, when fncA calls fncB, it does:

    jal    fncB
    

    The return address is placed in the [hardwired] register $ra

    At the end, normally, fncB returns via jr $ra.

    But, fncB may use any register in the jr instruction, so it could do:

    move $v0,$ra
    li   $ra,0
    jr   $v0
    

    Preserving $ra by callee for caller really has no meaning. $ra is where the called function will [normally] find the return address, but it can move it around, if it wishes.

    In fncA, it could do:

    jal   fncB
    jal   fncB
    

    The $ra value will be different in both cases, so it makes no sense to talk of preserving $ra for caller's benefit [as there is none].

    But it seems to me that $ra, a preserved register

    Preserved? By whom? Caller doesn't need the value [nor care about what happens to it as long as callee returns to the correct place]. A called function does not have to preserve $ra for caller. It has to preserve the return address [but, not necessarily in $ra] for itself.

    Thus, it's probably incorrect to think of $ra as preserved by caller or callee

    ... is caller saved as the caller saves the address to which it have to return.

    When caller [via jal] sets the return address in $ra, it really isn't saving it in the sense of saving registers [on the stack].

    If fncB calls another function fncC it usually preserves $ra and it usually saves it on the stack. But, it can preserve the register contents in other ways if it desires.

    Also, the jalr instruction could be used instead of jal [and is for very large address spans]. So, fncA could do:

    la    $t0,fncB
    jalr  $t0
    

    But, this is really just a shorthand for:

    la    $t0,fncB
    jalr  $ra,$t0
    

    But, if fncB is aware of how it's being called (i.e. we write the function differently), we could use a different register to hold the return address:

    la    $t0,fncB
    jalr  $t3,$t0
    

    Here $t3 will hold the return address. This is a non-standard calling convention (i.e. not ABI conforming).

    We might have a function fncD that fully conforms to the ABI. But, it might call several internal functions that no other function will call (e.g. fncD1, fncD2, ...). fncD is at liberty to call these functions with whatever non-standard calling conventions it chooses.

    For example, it may use $t0-$t6 for function arguments instead of $a0-$a3. If fncD preserves $s0-s7 at the outer edge, these could be used for function arguments to fncD1.

    The only registers that are absolutely hardwired are $zero and $ra. For $ra this is only because it is hardwired/implicit in the jal instruction. If we only used jalr, we could free up $ra as an ordinary register like $t0.

    The rest of the registers are not dictated by the CPU architecture, but merely the ABI convention.

    If we wrote a program in 100% assembler, wrote all our own functions, we could use any convention we wished. For example, we could use $t0 as our stack pointer register instead of $sp. That's because the mips architecture has no push/pop instructions where the $sp register is implicit. It only has lw/sw and we can use whatever register we want.

    Here is a program that demonstrates some of the standard and non-standard things you can do:

        .data
    msg_jal1:   .asciiz     "fncjal1\n"
    msg_jal2:   .asciiz     "fncjal2\n"
    msg_jalx:   .asciiz     "fncjalx\n"
    msg_jaly:   .asciiz     "fncjaly\n"
    msg_jalz:   .asciiz     "fncjalz\n"
    msg_jalr1:  .asciiz     "fncjalr1\n"
    msg_jalr2:  .asciiz     "fncjalr2\n"
    msg_post:   .asciiz     "exit\n"
    
        .text
        .globl  main
    main:
        # for the jal instruction, the return address register is hardwired to $ra
        jal     fncjal1
    
        # but, once called, a function may destroy it at will
        jal     fncjal2
    
        # double level call
        jal     fncjalx
    
        # jalr takes two registers -- this is just a shorthand for ...
        la      $t0,fncjalr1
        jalr    $t0
    
        # ... this
        la      $t0,fncjalr1
        jalr    $ra,$t0
    
        # we may use any return address register we want (subject to our ABI rules)
        la      $t0,fncjalr2
        jalr    $t3,$t0
    
        # show we got back alive
        li      $v0,4
        la      $a0,msg_post
        syscall
    
        li      $v0,10                  # syscall for exit program
        syscall
    
    # fncja11 -- standard function
    fncjal1:
        li      $v0,4
        la      $a0,msg_jal1
        syscall
        jr      $ra                     # do return
    
    # fncja12 -- standard function that returns via different register
    fncjal2:
        li      $v0,4
        la      $a0,msg_jal2
        syscall
    
        # grab the return address
        # we can preserve this in just about any register we wish (e.g. $a0) as
        # long as the jr instruction below matches
        move    $v0,$ra
    
        # zero out the standard return register
        # NOTES:
        # (1) this _is_ ABI conforming
        # (2) caller may _not_ assume $ra has been preserved
        # (3) _we_ need to preserve the return _address_ but we may do anything
        #     we wish to the return _register_
        li      $ra,0
    
        jr      $v0                     # do return
    
    # fncja1x -- standard function that calls another function
    fncjalx:
        # preserve return address
        addi    $sp,$sp,-4
        sw      $ra,0($sp)
    
        li      $v0,4
        la      $a0,msg_jalx
        syscall
    
        jal     fncjal1
        jal     fncjal2
    
        # restore return address
        lw      $ra,0($sp)
        addi    $sp,$sp,4
    
        jr      $ra                     # do return
    
    # fncja1y -- standard function that calls another function with funny return
    fncjaly:
        # preserve return address
        addi    $sp,$sp,-4
        sw      $ra,0($sp)
    
        li      $v0,4
        la      $a0,msg_jaly
        syscall
    
        jal     fncjal1
        jal     fncjal2
    
        # restore return address
        lw      $a0,0($sp)
        addi    $sp,$sp,4
    
        jr      $a0                     # do return
    
    # fncjalz -- non-standard function that calls another function
    fncjalz:
        move    $t7,$ra                 # preserve return address
    
        li      $v0,4
        la      $a0,msg_jalz
        syscall
    
        jal     fncjal1
        jal     fncjal2
    
        jr      $t7                     # do return
    
    # fncjalr1 -- standard function [called via jalr]
    fncjalr1:
        li      $v0,4
        la      $a0,msg_jalr1
        syscall
        jr      $ra                     # do return
    
    # fncjalr2 -- non-standard function [called via jalr]
    fncjalr2:
        li      $v0,4
        la      $a0,msg_jalr2
        syscall
        jr      $t3                     # do return
    

    The output of this program is:

    fncjal1
    fncjal2
    fncjalx
    fncjal1
    fncjal2
    fncjalr1
    fncjalr1
    fncjalr2
    exit