Why $ra is Caller Saved in RISC-V
Asked Answered
B

3

5

I find that in RISC-V, ra is caller saved, in MIPS, ra is callee, which means in RISC-V callee can directly change the value in ra without save, but since ra has changed, how callee return back to caller?

Breakneck answered 11/1, 2020 at 9:52 Comment(1)
Can you site a reference that says the MIPS $ra register is callee saves? All the references I'm looking at say only that the $s registers are saved/preserved.Resultant
R
3

The usage of RISC V ra and MIPS $ra is effectively the same regardless of the designation.

Since both caller (who needs to return to their caller) and (a non-leaf) callee need to repurpose the return address register, the value in that register needs to be preserved.  The only logical way to do that is to preserve the register once on entry and restore it once on exit just like the s/$s preserved, callee-saves registers.

However, once thus saved, the return address register may be repurposed by functions for other uses and any such usage would follow caller saves conventions (unlike the $s registers, which are guaranteed to be preserved across a call).

So, effectively, ra/$ra can behave, at different times, both as callee saves, and caller saves.

A caller cannot rely on a value placed into ra/$ra surviving a function call (as they could with $s registers), thus is is caller saves.  Yet, when a callee preserves ra/$ra, it preserves it just like it does the $s callee-saves registers — namely in prologue/epilogue.

By contrast, $t registers, if preserved by the caller so as to survive a function call, would have to be preserved after each update to the value (e.g. minimally after the first initialization), and this is caller saves behavior.  These registers are initialized first, then preserved, whereas $s registers are preserved first, then initialized.

ra/$ra has behaviors of both callee and caller saves: it needs to be preserved before being initialized and reused/repurposed, which is a callee saves approach, yet, a variable placed into $ra would not survive a function call, and so to survive a function call, would need to be initialized then preserved.

Resultant answered 11/1, 2020 at 16:6 Comment(2)
Yes, when calee call another func, it become caller, but what if callee just want to use ra to do some calculation, and since it is caller saved, does this means it can directly use ra without save?Breakneck
A value (the return address) is passed from caller to callee in ra/$ra. (In this sense it is like a parameter, e.g. a0/$a0.) This value is needed at the end of the function (unless, say, the function intends never returns). If a function wants to use the ra/$ra register for some other use, it will have to preserve the value passed in, for its later use. However, it does not have to restore it specifically back to the ra/$ra register, it just has to use the original value passed in, in order to return to the caller at the right location.Resultant
P
4

I find that in RISC-V, ra is caller saved

The fact that ra is a caller-saved register means that the caller can't assume that its value is preserved when the control flow returns to it. Therefore, if the caller wants to preserve ra, it has to save ra before transferring the control to the callee.

Transferring control to subroutines can be achieved by jal and jalr. They both load the address of the following instruction – the return address – into the destination register (usually ra). So, in the general case:

  • ra is clobbered at the moment of calling a subroutine.
  • ra contains the return address to go back to the current subroutine's caller.

The first point implies that the register ra isn't be preserved between calls. So, if a subroutine wants to maintain ra – the return address to its caller subroutine – when calling a subroutine, it must save ra before performing the call.

in RISC-V callee can directly change the value in ra without save, but since ra has changed, how callee return back to caller?

If the callee loses the return address to its caller, there is no way to return to the caller. That's why ra has to be saved before a call because it is clobbered when performing a call.

Petaloid answered 11/1, 2020 at 15:20 Comment(4)
In [UCB CS61C MIPS version Page 31][inst.eecs.berkeley.edu/~cs61c/sp15/lec/06/…, $ra is in register that "Preserved across funcIon call". In [UCB CS61C RISC-V version Page 43][inst.eecs.berkeley.edu/~cs61c/sp18/lec/06/lec06.pdf], $ra is in "Not preserved cross function call". I'm curious why it was modified.Breakneck
The links are broken. jal/jalr both modify ra unless a different destination register is specified. These instructions for calling don't write into memory (i.e., pushing a register into the stack), so they don't save the current return address into memory. There is a pure separation between branch and memory-accessing instructions. It is also convenient: the return address is not saved to memory – it is slower – if it is not needed (e.g., leaf subroutines).Petaloid
Sorry, I just want to use markdown style, the "]" cause fail, try following: (1) MIPS Page 31 inst.eecs.berkeley.edu/~cs61c/sp15/lec/06/… (2) RISC-V Page 43 inst.eecs.berkeley.edu/~cs61c/sp18/lec/06/lec06.pdfBreakneck
Transferring control works in the same way in MIPS. In particular, $ra is clobbered. This other question may help you. The value in $ra is only relevant to a single function.Petaloid
R
3

The usage of RISC V ra and MIPS $ra is effectively the same regardless of the designation.

Since both caller (who needs to return to their caller) and (a non-leaf) callee need to repurpose the return address register, the value in that register needs to be preserved.  The only logical way to do that is to preserve the register once on entry and restore it once on exit just like the s/$s preserved, callee-saves registers.

However, once thus saved, the return address register may be repurposed by functions for other uses and any such usage would follow caller saves conventions (unlike the $s registers, which are guaranteed to be preserved across a call).

So, effectively, ra/$ra can behave, at different times, both as callee saves, and caller saves.

A caller cannot rely on a value placed into ra/$ra surviving a function call (as they could with $s registers), thus is is caller saves.  Yet, when a callee preserves ra/$ra, it preserves it just like it does the $s callee-saves registers — namely in prologue/epilogue.

By contrast, $t registers, if preserved by the caller so as to survive a function call, would have to be preserved after each update to the value (e.g. minimally after the first initialization), and this is caller saves behavior.  These registers are initialized first, then preserved, whereas $s registers are preserved first, then initialized.

ra/$ra has behaviors of both callee and caller saves: it needs to be preserved before being initialized and reused/repurposed, which is a callee saves approach, yet, a variable placed into $ra would not survive a function call, and so to survive a function call, would need to be initialized then preserved.

Resultant answered 11/1, 2020 at 16:6 Comment(2)
Yes, when calee call another func, it become caller, but what if callee just want to use ra to do some calculation, and since it is caller saved, does this means it can directly use ra without save?Breakneck
A value (the return address) is passed from caller to callee in ra/$ra. (In this sense it is like a parameter, e.g. a0/$a0.) This value is needed at the end of the function (unless, say, the function intends never returns). If a function wants to use the ra/$ra register for some other use, it will have to preserve the value passed in, for its later use. However, it does not have to restore it specifically back to the ra/$ra register, it just has to use the original value passed in, in order to return to the caller at the right location.Resultant
B
1

The value in ra serves as a special argument to the callee. So as with other arguments, it is meaningful to make it caller-saved.

In most cases, the caller has nothing to do with ra other than passing it to the callee, which means the caller usually doesn't need to preserve the value in ra across function calls. So making ra caller-saved incurs no performance cost. This is also similar to all other general arguments.

The callee can of course repurpose argument registers, including ra, as long as it has finished using their values. But for ra, its value is used at the very end of the call. Therefore, the callee has to preverve its value for its own use, instead of for the caller.

Blader answered 14/8, 2024 at 6:58 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.