MIPS架构中的$ra寄存器是由调用者保存还是被调用者保存?

6
我读到保留寄存器是由调用者保存的,而非保留寄存器是由被调用者保存的。但是,似乎 $ra(一个保留寄存器)是由调用者保存的,因为调用者保存了返回地址。有人可以解释一下我错在哪里吗?
2个回答

7
我读到过保留寄存器是主调者保存的,非保留寄存器是被调者保存的。这可能不是表述的最佳方式,并可能是混淆的源泉。以下是更好的表述方式:
一个函数(即被调用方)必须保存 $s0-$s7 寄存器、全局指针 $gp、栈指针 $sp 和帧指针 $fp。其他所有寄存器可以根据函数需要随意更改。
例如,当 fncA 调用 fncB 时,它会执行:
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

1
当您使用指令jaljalr调用子程序(即“函数”)时,返回地址会被自动存储在$ra寄存器中。
如果您已经在一个子程序中,在执行jaljalr后,您将失去返回地址的值;因此,当使用ret指令返回时,可能会出现Segmentation fault。因此,在调用子程序(或更一般地在使用jalrjal之前),您应该将值保存在$ra寄存器的某个不会立即被覆盖的位置。

2
MIPS 指令集中 没有 ret 指令。在简单情况下,等效的指令是 jr $ra - Craig Estey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接