函数与栈
函数是一段有名称的指令块,你可以多次使用它。你跳转到它,它执行自己的任务,然后把控制权返回到你调用它的地方。这样你就可以只编写一次逻辑,并在许多地方使用它,而不必在整个程序中重复相同的代码行。
使用 来调用函数:它会跳转到该标签,并把返回位置存储在 ra 中。函数以 结束,它会跳回 ra 中的地址。 和 是伪指令:它们是方便的名称,汇编器会将其展开为实际指令 和 。
参数放在 a0、a1 以及其后的寄存器中。返回值放在 a0 中。这种安排就是调用约定,它是让不同代码片段协同工作的共同约定。
一个 square() 函数
li a0, 6 # 参数call square # 调用,然后 ra 记住要返回到哪里li a7, 1 # square 将结果留在 a0 中ecallli a7, 10ecallsquare:mul a0, a0, a0 # a0 = a0 * a0ret # 跳回 ra
跟随这次调用。 把参数 6 放入 a0。 跳转到 square 标签,并把返回地址存储在 ra 中。在函数内部, 计算 6 * 6 = 36,并把结果留在 a0 中,也就是用于返回值的寄存器。 跳回调用之后的那条指令,在那里 和 打印 36,然后程序退出。
函数调用与返回
caller
call A
...
A
addi sp, sp, -4
sw ra, 0(sp)
call B
lw ra, 0(sp)
addi sp, sp, 4
ret
B
mul a0, a0, a0
ret
STACK
sp →
0x7ffffff0
0x7fffffec
调用者即将调用 A。
caller 执行 ,这会自动将返回点保存到 ra 寄存器中的 caller。在 A 能执行 之前,它遇到了一个问题: 指令即将用新的返回地址覆盖 ra,从而破坏 A 返回原处所需的地址。为了解决这个问题,A 在栈上腾出空间()并将原始的 ra 保存到那里()。现在可以安全地 了。B 运行并通过普通的 返回,落回到 A 内部。最后,A 从栈中恢复原始的 ra(),清理栈指针(),它自己的 使用恢复的值成功跳回 caller。如果 A 跳过了这一步, 指令将永久覆盖 ra,而 A 最后的 将跳到错误的位置而不是返回原处。堆栈
由于寄存器数量有限,因此调用其他函数的函数需要临时存储空间。栈是您通过栈指针 sp 借用的内存:通过减法操作腾出空间,存储需要保留的数据,并在 之前将其恢复。
栈是一种后进先出(LIFO)结构:最近存储的值会最先被取出。在 RISC-V 中,栈在内存中向下增长。从 sp 中减去数值会保留可安全使用的新空间。当你取回值时,必须按照与存储时相反的顺序进行,否则程序的执行流会被破坏。
在调用之间保存和恢复 ra
addi sp, sp, -4 # 为一个字腾出空间sw ra, 0(sp) # 保存返回地址# ... 在此调用其他函数 ...lw ra, 0(sp) # 恢复它addi sp, sp, 4 # 释放该空间ret
每个 / 都会覆盖 ra,所以如果你的函数调用另一个函数,内部调用会破坏你的返回地址,你自己的 就会跳到错误的位置。为防止这种情况, 在栈上保留 4 个字节, 把 ra 保存在那里。内部调用结束后, 恢复返回地址, 在 之前归还空间。
⚠
如果你调用过任何东西,请在 之前恢复
ra,并且始终把 sp 恢复到开始时的值。栈与起始状态不一致,是汇编程序崩溃的常见原因。