riscv 函数调用问题
前言
昨天开组会,助教在会上问了一个问题:A 函数调用 B 函数,s0 的保存是哪一个函数进行的。我觉得这个问题挺熟悉的,因为之前在看 riscv 的 xv6 操作系统时遇到过。
实验
main.c:
1 |
|
add.h、add.c:
1 |
|
Makefile:
1 |
|
在编译好了之后:
1 |
|
可以看到,main 函数在刚开始:
1 |
|
首先修改 sp,由于 riscv 的栈空间是向下增长的,这是分配了 32 字节的空间。然后将寄存器 ra、s0 保存到 main 空间,接着重置 s0 为 main 栈空间起始地址。
我们一般把这个叫做前言。
sp(栈指针):通常指向当前栈的顶部,管理局部变量、临时数据、返回地址等。sp 在函数调用时减少,为当前函数的局部变量和参数分配空间。
s0(帧指针):用于指向栈帧的基准位置,便于访问局部变量和参数。它在函数执行期间保持不变,以方便访问栈中的数据。
这个 ra (return address)是 main 函数的返回地址,因为 main 中也要进行函数调用,而这个过程需要修改 ra 以让 add 函数能正确返回。因此在前言中也要把 ra 保存起来。
在 main 函数通过 jal 调用 add 之后,首先它会把返回地址,也就是 278:fea42623 sw a0,-20(s0)
这个指令的地址放到 ra 中。然后无条件跳转到 1d8
。
跳到 add 之后,依然要进行函数前言:
1 |
|
它依然会设置栈帧、帧指针。但是它并没有保存 ra,这是因为 add 函数没有进行函数调用。
执行完了之后,它就会进入后记:
1 |
|
可以看到,它会把返回值放到 a0,然后恢复调用它的函数 main 的 sp、s0,然后返回。
函数返回相当简单,它的 rd 为 0 号寄存器,这意味着它不用保存返回地址,即它是一个函数返回,不是一个函数调用。另外,它的参数 ra 和 ra 是一个值,这意味着它确实要返回。
下面的C 语言描述更能说明这两个指令的情况:
1 |
|
同样,返回之后,就到了 main 函数的这里:
1 |
|
可以看到,它首先保存返回值,然后把返回值重新放到 a0。之后就是启动函数(调用 main 的函数)的返回了,恢复 ra、s0、sp之后,直接返回。
可以看到,main 函数调用 add 之后, sp 的开辟、s0 的保存、s0 的设置,都是 add 函数进行的。遵守着谁使用,谁负责整理和维护
的规则。比如,main 要调用函数,那么它就得保存 ra,然后给 ra 中放置add的返回地址;add 要返回,那么add 就得恢复 s0、sp。