一、先明确几个关键概念

在 RISC-V 中:

名称 寄存器 作用
sp Stack Pointer 指向当前栈顶(栈向低地址增长)
s0 / fp Frame Pointer 指向当前函数栈帧的基地址(通常是高地址一端)
ra Return Address 调用函数时保存返回地址的寄存器

二、栈方向与函数调用的基本规律

RISC-V 的栈是向下增长的:

高地址
│
│  ← 栈向下增长(sub sp)
│
└── 低地址

所以每次函数调用要在栈上“开一块空间”给局部变量、保存寄存器等。


三、逐行解释你的代码

addi sp, sp, -64
  • 意思sp = sp - 64
  • 作用:在栈上“申请”了 64 字节的空间。
  • 因为栈向下增长,所以是减。
  • 这块空间通常用于保存:
    • 调用者保存的寄存器(ra、s0 等)
    • 函数局部变量

此时栈长这样:

高地址
│
│    ← s0 (上一层的 frame pointer)
│    ← ra (返回地址)
│
sp → [ 空出的 64 字节空间底部 ]
低地址

sd  ra, 56(sp)
sd  s0, 48(sp)
  • sd 是 “store doubleword”(存 8 字节)的意思。
  • sd ra, 56(sp):把返回地址 ra 存到 sp + 56 这个位置。
  • sd s0, 48(sp):把上一层函数的 s0(frame pointer)存起来,等函数结束时再恢复。

也就是说,这两条是在保存调用者状态,避免被当前函数覆盖。


addi s0, sp, 64
  • 现在要设置新的 frame pointer(帧指针)。
  • 意思s0 = sp + 64
  • 因为刚才我们向下分配了 64 字节空间,而 sp 指向栈底。
  • 所以 sp + 64 就是当前函数栈帧的高地址端,作为当前函数的帧基址。

此时栈的逻辑结构是这样的:

高地址
│
│   ↑ s0 指向这里(frame pointer)
│   ----------------------------
│   保存区:
│      56(sp): ra(返回地址)
│      48(sp): s0(上一个帧指针)
│   ----------------------------
│   其他局部变量 / 临时空间
│   ...
│   sp → 当前栈底(低地址端)
│
低地址

四、函数返回时(epilogue)

对应地,在函数结束时,会有“出栈”操作恢复寄存器:

ld  ra, 56(sp)      # 恢复返回地址
ld  s0, 48(sp)      # 恢复上层帧指针
addi sp, sp, 64     # 回收栈空间
ret                 # 跳回调用者

五、整体流程(简化版)

阶段 操作 汇编 作用
进入函数 申请栈帧 addi sp, sp, -64 分配栈空间
进入函数 保存调用者上下文 sd ra, 56(sp) / sd s0, 48(sp) 防止覆盖调用者寄存器
进入函数 设置新帧指针 addi s0, sp, 64 方便访问局部变量
函数结束 恢复调用者上下文 ld ra, 56(sp) / ld s0, 48(sp) 返回前恢复状态
函数结束 回收栈空间 addi sp, sp, 64 恢复栈指针
函数结束 返回 ret 跳转到 ra

六、总结一句话

这三条指令的作用是: 为当前函数创建一个新的栈帧,保存调用者状态,并建立新的帧指针以供访问局部变量。

对应概念:

  • sp 管理栈空间(动态变化);
  • s0(frame pointer)是该函数栈帧的“固定参考点”。