函数和栈
我们通过下面这张图来整体理解函数调用和栈的关系: 
这张图非常经典,它清晰地展示了计算机程序在执行函数调用时,内存中栈(Stack)的结构和工作原理。图中描述了一个场景:一个名为 DrawSquare 的函数在执行过程中,调用了另一个名为 DrawLine 的函数。
我们先理解下面两个概念: 函数调用栈 (Call Stack) 和 栈帧 (Stack Frame)。
函数调用栈 (Call Stack):一块特殊的内存区域,遵循后进先出 (LIFO, Last-In, First-Out) 的原则。每当一个函数被调用,就会在栈顶为其分配一块内存;当函数返回时,这块内存会被释放。
栈帧 (Stack Frame):每次函数调用时在栈上创建的这块专属内存区域,就称为一个栈帧。它包含了该次函数调用所需的所有信息。每个栈帧通常包含三个主要部分, 且它们按照以下顺序从栈底到栈顶排列:
参数 (Parameters):存放调用者函数传递给被调用函数的值。在图中, Parameters for DrawSquare 是调用 DrawSquare 时传入的参数。Parameters for DrawLine 是 DrawSquare 函数调用 DrawLine 时传入的参数。
返回地址 (Return Address): 这是至关重要的一部分。它存储了函数调用指令的下一条指令在内存中的地址。当被调用的函数执行完毕后,CPU 就通过读取这个返回地址,知道应该跳转回哪里继续执行调用者函数的代码。在图中, DrawLine 栈帧中的 Return Address 指向 DrawSquare 函数中调用 DrawLine 语句的下一行代码。
局部变量 (Locals): 存放函数内部定义的局部变量。这些变量在函数开始执行时创建,在函数返回时被销毁,其生命周期与函数调用绑定。在图中, Locals of DrawLine 存放的是 DrawLine 函数内部声明的变量。
图中展示了两个栈帧,DrawLine 的栈帧位于 DrawSquare 的栈帧之上(在内存地址较低的一侧),这表明 DrawSquare 函数调用了 DrawLine 函数。
除此之外, 图中还标示了两个非常重要的 CPU 寄存器,它们是管理栈的关键。
栈指针/栈顶指针 (Stack Pointer, SP)
作用:始终指向栈的顶部 (top of stack)。在图中,它指向 DrawLine 栈帧中局部变量区域的末端。
行为:当函数分配局部变量或进行新的函数调用(压栈,push)时,SP 会向低地址方向移动(栈增长)。当函数返回(出栈,pop)时,SP 会向高地址方向移动(栈收缩)。
帧指针 (Frame Pointer, FP)
作用:也称为基址指针 (Base Pointer, BP)。它指向当前活动栈帧(即位于栈顶的, 最后被调用且正在执行的那个函数)的一个固定位置,通常是栈帧的底部(或返回地址和参数之间的边界)。
意义:FP 提供了一个稳定的基准地址。在函数执行期间,SP 可能会因为局部变量的动态分配而移动,但 FP 保持不变。因此,程序可以通过 FP 以固定的偏移量来准确地访问参数(正向偏移)和局部变量(负向偏移),而不受 SP 移动的影响。
结合这张图,我们可以还原整个函数调用的动态过程:
调用 DrawSquare:DrawSquare 的参数被压入栈中。
调用指令 call DrawSquare 执行,将返回地址(main 函数或其他调用者的下一条指令地址)压入栈中。
DrawSquare 函数开始执行,创建自己的栈帧,分配局部变量空间。FP 和 SP 指针更新,指向这个新栈帧。
DrawSquare 调用 DrawLine (图示的状态):DrawSquare 将调用 DrawLine 所需的参数压入栈中。
执行 call DrawLine 指令,将返回地址(DrawSquare 中的下一条指令地址)压入栈中。
DrawLine 函数开始执行,在 DrawSquare 栈帧的上方创建自己的栈帧,并分配局部变量空间。
FP 和 SP 指针再次更新,指向最顶部的 DrawLine 栈帧。这正是图中捕捉到的瞬间。
DrawLine 返回:DrawLine 函数执行完毕, 它的栈帧被销毁(出栈)。SP 和 FP 会恢复到指向 DrawSquare 栈帧的状态。CPU 读取 DrawLine 栈帧中保存的返回地址,并跳转到该地址,回到 DrawSquare 中继续执行。
DrawSquare 返回:DrawSquare 执行完毕,其栈帧被销毁,控制权返回给最初的调用者。