面试题答案
一键面试函数调用栈的动态变化
- 栈帧创建:
- 当一个函数被调用时,首先会在栈上为该函数创建栈帧。在Go语言程序编译为汇编语言后,一般会通过
CALL
指令调用函数。在CALL
指令执行前,会将返回地址压入栈中。返回地址是调用函数后继续执行的下一条指令的地址。 - 例如,假设主函数
main
调用函数func1
,当执行到调用func1
的CALL
指令时,会将main
函数中调用func1
之后的指令地址压入栈。 - 栈帧的大小通常由函数内定义的局部变量大小以及可能需要的额外空间(如保存寄存器的值等)决定。在Go语言的汇编实现中,编译器会计算好栈帧的大小。
- 当一个函数被调用时,首先会在栈上为该函数创建栈帧。在Go语言程序编译为汇编语言后,一般会通过
- 变量压栈:
- 参数压栈:函数调用前,参数会按照一定顺序压入栈中。在Go语言中,参数压栈顺序取决于编译器实现,但一般会从右往左压栈(与常见的C语言约定类似)。例如,如果函数
func1(a, b, c)
被调用,那么c
会先压栈,然后是b
,最后是a
。这些参数会被压入调用者的栈帧区域。 - 局部变量压栈:在函数内部,局部变量会在栈帧内分配空间。对于复杂的变量操作,如果变量是在栈上分配的(而不是在堆上通过
new
等操作分配),它们会根据其类型和大小在栈帧内依次分配空间。例如,定义一个int
类型的局部变量x
,会在栈帧内预留4字节(假设32位系统)的空间,然后可能会通过MOV
等指令将值存入该空间。 - 在递归函数调用的情况下,每次递归调用都会重复上述参数压栈和局部变量在新栈帧内分配空间的过程。每个递归调用都有自己独立的栈帧,虽然函数代码相同,但变量值相互独立。
- 参数压栈:函数调用前,参数会按照一定顺序压入栈中。在Go语言中,参数压栈顺序取决于编译器实现,但一般会从右往左压栈(与常见的C语言约定类似)。例如,如果函数
- 函数执行与栈操作:
- 在函数执行过程中,可能会对栈上的变量进行各种操作,如读取、修改等。例如,通过
MOV
指令从栈上读取变量值到寄存器,或者将寄存器中的值通过MOV
指令存回栈上变量的位置。 - 如果函数内部调用其他函数(嵌套调用),同样会重复栈帧创建、参数压栈等过程。内层函数的栈帧会在当前函数栈帧之上创建,随着嵌套深度增加,栈会不断增长。
- 在函数执行过程中,可能会对栈上的变量进行各种操作,如读取、修改等。例如,通过
- 栈帧销毁与变量出栈:
- 当函数执行完毕准备返回时,会销毁栈帧。首先,会从栈中弹出返回地址,通过
RET
指令返回到调用者的代码处继续执行。 - 栈帧内的局部变量会随着栈帧的销毁而从栈上移除。这个过程并不是显式地一条一条指令去清除每个变量,而是通过调整栈指针(如
ESP
或RSP
,不同架构不同)来实现。当栈指针调整回函数调用前的位置时,栈帧内的局部变量空间就被释放了。 - 对于参数,它们的空间释放也是通过调整栈指针完成。在函数返回后,调用者的栈指针会恢复到调用函数前的状态,从而释放参数占用的栈空间。在递归函数返回时,每一层递归调用的栈帧都会依次销毁,栈会不断收缩,直到回到最初的调用点。
- 当函数执行完毕准备返回时,会销毁栈帧。首先,会从栈中弹出返回地址,通过