函数调用约定中寄存器的保存问题
函数调用约定中寄存器的保存问题
calling convention
Created: 2024-03-25T17:03+08:00
Published: 2024-03-29T09:50+08:00
Categories: Compiler
一个方便的在线查看汇编网站:Compiler Explorer
函数调用时,caller 和 callee 的寄存器保存问题。
callee 至少能看到:
- local variables:自己的局部变量
- function arguments:调用自己的函数参数
- return address: 要返回到的 caller 的地址
基于 risc-v(32-bits) gcc(trunk),执行过程中:
- local variables 由 stack pointer(sp 寄存器)索引
- function arguments 由 frame pointer(fp 寄存器,也叫做 s0)索引
- caller 的信息,如 return address 和 caller 的 fp 在 frame pointer 下面,sp 指向的栈区高地址
例子
int foo(int p0, int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, int p9, int p10) {
int a = 1;
int b = 2;
return p0 + p1 + a;
}
int main() {
foo(0,1,2,3,4,5,6,7,8,9,10);
return 0;
}
对应汇编如下:
# call foo(0,1,2,3,4,5,6,7,8,9,10) in main():
li a5,10
sw a5,8(sp)
li a5,9
sw a5,4(sp)
li a5,8
sw a5,0(sp)
li a7,7
li a6,6
li a5,5
li a4,4
li a3,3
li a2,2
li a1,1
li a0,0
call foo(int, int, int, int, int, int, int, int, int, int, int)
- 函数参数压栈的顺序是从后往前,10 先被压栈,然后是 9 ……
- 把 foo 的参数放到 sp 上,sp[0] 就是 倒数第一个参数,sp[1] 是倒数第二个参数,……
这样 foo 作为 callee,看到的内存和寄存器的场景就是:
- 前 8 个参数在寄存器上
- sp 指向了函数第 8 个以后的参数
说明现在 callee 的参数是由 sp 索引的
risc-v 32-bits gcc(trunk) 这样处理 fp 和 sp:
- local variables 由 stack pointer 索引
- function arguments 由 frame pointer 索引
- caller 的 ra 等存储在 sp 指向的栈区高地址
callee 要执行 fp=sp
,这样就可以用 fp 索引调用参数了,
但是现在 foo 看到的 fp 是前文 main 的函数参数索引,
如果直接执行 fp=sp
,原来 fp 的值就被覆盖了,所以要把 fp 保存起来。
这就是 foo 函数开头的逻辑:
- 存储 caller 的 ra 和 fp
- 修改 fp 和 sp 方便自己调用
foo 最后的逻辑就是把 fp、ra 恢复到 main 调用前的状态
foo(int, int, int, int, int, int, int, int, int, int, int):
addi sp,sp,-64 # save a stack for local variables
sw ra,60(sp)
sw s0,56(sp)
addi s0,sp,64 # adjust fp to previous stack pointer to index arguments
sw a0,-36(s0)
sw a1,-40(s0)
sw a2,-44(s0)
sw a3,-48(s0)
sw a4,-52(s0)
sw a5,-56(s0)
sw a6,-60(s0)
sw a7,-64(s0)
li a5,1
sw a5,-20(s0)
li a5,2
sw a5,-24(s0)
lw a4,-36(s0)
lw a5,-40(s0)
add a4,a4,a5
lw a5,-20(s0)
add a5,a4,a5
mv a0,a5 # result in a0
lw ra,60(sp)
lw s0,56(sp) # restore register fp for caller
addi sp,sp,64 # restore register sp for caller
jr ra
caller-save 和 callee-save
callee-save 的意思是说,如果 callee 想要用这个寄存器,那么就要预先保存好里面的值,用完了以后恢复。
比如 fp 和 sp 就是 callee-save,caller 将这两个寄存器交给 callee,callee 为了自己的方便,调整它们的位置,
但是最后返回的时候,fp 和 sp 要像调用 callee 之前一样。
所以,callee-save 的寄存器在函数调用前后保持不变。