为什么通常把返回地址的值存放在相对于栈指针偏移量为12的位置?为什么不能简单地把它存放在栈的前4 个字节?
我在尝试写一个RISC-V程序,它需要从一个函数调用另一个函数。我理解到,在调用第二个函数时,第一函数的返回地址会丢失,因此我们把它存放在栈中。据我所理解,栈被初始化为16字节大小的原因,是官方RISC-V文档中的一个约定:
在标准的RISC-V调用约定中,栈向下生长,栈指针始终保持16字节对齐。
上述引文的来源链接:https://riscv.org/wp-content/uploads/2024/12/riscv-calling.pdf
我尝试参考一些资料来理解返回地址是如何存储的,大多数资料都是这样初始化栈并把地址存入其中:
addi sp, sp, -16
sw ra, 12(sp)
在这段代码中,我不明白为什么要使用偏移量12来存储寄存器 ra 的值(也就是返回地址),为什么不能把它存到栈的前4 个字节中?
我附上我正在编写的代码以供参考:
.section .text
.global _start
_start:
li a0, 1
li a1, 2
li a2, 3
jal ra, fun1
sub a0,a0,a1
ebreak
fun1:
add a0,a0,a1
addi sp, sp, -16
sw ra, 12(sp)
jal ra, fun2
lw ra, 12(sp)
addi sp, sp, 16
jalr zero, 0(ra)
fun2:
add a0,a0,a2
jalr zero, 0(ra)
解决方案
add sp, sp, -16 保留16字节。把返回地址存到 12(sp) 之后的位置,会使它位于栈帧的最顶端,看起来是一个合适的位置。所有本地变量都可以放在它下面,以及那些不全能放入寄存器的被调用者的栈参数也放在下面。
(如果这是一个叶函数,则无需保存/恢复返回地址,它可以像在你的 fun2 那里一样一直保留在链接寄存器中。)
栈向下增长,因此 12(sp) 就是 栈的前4 个字节。 (对齐要求会在你不需要为局部变量留出空间时,强制你分配比实际需要的空间。)
在一个带有部分栈参数的变参函数中,可能更有意义把返回地址放在别处,这样你就可以把寄存器参数与栈参数连续放在一起,形成一个你可以索引的数组。
关于栈帧布局的RISC-V调用约定要求,它限定了你应该在何处保存返回地址。
如果你也使用帧指针,ELF调用约定 标准化 一个特定的栈帧布局——在使用FP时会把保存的 fp 放在保存的返回地址旁边,这样栈展开/回溯就可以像链表一样跟随它,而不必了解每个函数栈帧的其他布局细节。
(在x86上,call 本身会把返回地址推到栈上,而不是像在RISC指令集那样写入寄存器,通常的做法是在使用帧指针时把调用方的帧指针寄存器就放在它下面。这也是RISC-V的通常做法,即把它放在返回地址之下。RISC-V的约定还要求把它们放在栈帧的最顶端,并且让 fp 等于传入的 sp。除了在变参函数中。)