jal fncB
返回地址被放置在[硬编码]寄存器$ra
中。
通常情况下,fncB
通过jr $ra
返回。
但是,fncB
可以在jr
指令中使用任何寄存器,因此它可以执行以下操作:
move $v0,$ra
li $ra,0
jr $v0
保留$ra
寄存器给调用者并没有实际意义,$ra
是被调用函数[通常]会找到返回地址的地方,但如果它愿意的话,它可以把它移动到其他地方。
在fncA
中,它可以这样做:
jal fncB
jal fncB
$ra
的值在这两种情况下是不同的,因此谈论为了调用者利益而保留$ra
毫无意义(因为没有调用者)。被调用函数不必为调用者保留$ra
,它必须为自己保存返回地址(但不一定在$ra
中保存)。
因此,把$ra
看作是由调用者或被调用者保存的可能是不正确的。
当调用者(通过jal
)在$ra
中设置返回地址时,它实际上并不是像保存寄存器那样“保存”它(即保存在堆栈中)。
如果fncB
调用另一个函数fncC
,通常会保留$ra
并将其保存在堆栈中。但是,如果它希望,它可以以其他方式保留寄存器内容。
此外,也可以使用jalr
指令代替jal
(对于非常大的地址跨度而言),因此fncA
可以这样做:
la $t0,fncB
jalr $t0
la $t0,fncB
jalr $ra,$t0
但是,如果fncB
知道它是如何被调用的(也就是说,我们编写函数时不同),我们可以使用不同的寄存器来保存返回地址:
la $t0,fncB
jalr $t3,$t0
这里$t3
将保存返回地址。这是一种非标准的调用约定(即不符合ABI)。
我们可能有一个完全符合ABI的函数fncD
,但它可能调用几个其他函数,而其他函数不会调用这些函数(例如fncD1、fncD2等
)。fncD
可以选择任何非标准的调用约定来调用这些函数。
例如,它可以使用$t0-$t6
作为函数参数,而不是$a0-$a3
。如果fncD
在外层保留了$s0-s7
,那么这些寄存器可以用于fncD1
的函数参数。
唯一绝对硬连接的寄存器是$zero
和$ra
。对于$ra
,这仅仅是因为它在jal
指令中被硬连接/隐含。如果我们只使用jalr
,我们可以像使用$t0
寄存器一样释放$ra
寄存器。
其余的寄存器并不是由CPU架构决定的,而仅仅是ABI约定。
如果我们使用100%汇编语言编写程序,编写自己的所有函数,我们可以使用任何我们想要的约定。例如,我们可以将$t0
寄存器作为堆栈指针寄存器,而不是$sp
。这是因为MIPS架构没有隐含$sp
寄存器的push/pop指令,它只有lw/sw
指令,我们可以使用任何我们想要的寄存器。
下面是一个演示一些标准和非标准操作的程序:
.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
fncjal1
fncjal2
fncjalx
fncjal1
fncjal2
fncjalr1
fncjalr1
fncjalr2
exit
jal
或jalr
调用子程序(即“函数”)时,返回地址会被自动存储在$ra
寄存器中。jal
或jalr
后,您将失去返回地址的值;因此,当使用ret
指令返回时,可能会出现Segmentation fault
。因此,在调用子程序(或更一般地在使用jalr
或jal
之前),您应该将值保存在$ra
寄存器的某个不会立即被覆盖的位置。
ret
指令。在简单情况下,等效的指令是jr $ra
。 - Craig Estey