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