Whether $ra register callee saved or caller saved

2020-02-15 06:41发布

问题:

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?

回答1:

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


回答2:

When you call a subroutine with the instructions jal or jalr, the return address is stored in $ra so if you are already in a subroutine, you will loose the value and so when returning with ret instruction, you might have a Segmentation fault. So before calling a subroutine (or more generally before using jalr or jal), you should save the $ra register