一、先明确几个关键概念
在 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)是该函数栈帧的“固定参考点”。