众所周知,C 语言中的函数调用是通过栈实现的,但该过程具体是如何发生的,往往被学习时囫囵吞枣的人,也就是我,忽略。本文对函数调用栈的相关知识点稍加梳理总结,主要内容来自教材,和参考博客 程序的内存布局——函数调用栈的那点事,本文读者可直接移步原博客获得更优质阅读体验。
1. 函数
后面将使用如下的示例程序进行讨论:
int foo(int a, int b, int c)
{
int i, j, k;
return 1;
}
int main(void)
{
foo(1,2,3) ;
return 0 ;
}
2. ESP 与 EBP
分析函数调用在内存中发生了什么之前,应明确一些概念。
栈的概念不用赘述,其常见操作进栈 (push) 与出栈 (pop) 分别指将数据压入栈顶,以及将数据从栈顶释放。该过程是通过移动栈顶位置实现的,而栈底位置在栈创建后即保持不变,直至栈被撤销。
这就涉及到了两个寄存器:EBP 与 ESP。EBP为栈底寄存器,存放栈底地址,而ESP为栈顶寄存器,存放栈顶地址,ESP 存放的数据变动频繁,每次压栈出栈均会发生变化;而 EBP 只在函数调用时,随着新函数栈的建立改变其存放的数值。
3. 当函数调用的时候发生了什么
当方法 main 需要调用 foo 时,它的标准行为:
- 在 main 方法的调用栈中,将 foo 的参数 从右向左 依次 push 到栈中
- 把 main 方法当前指令的 下一条指令地址 ,即被调函数执行结束后的返回地址,入栈,隐藏在 call 指令中
- 使用 call 指令调用目标函数体 foo
请注意,以上 3 步都处于 main 的调用栈。接下来,在 foo 函数中:
- 将 ebp 的当前值 push 到栈中,即保存 ebp。
- 将当前 esp 的值赋给 ebp ,此时意味着进入了 foo 方法的调用栈。
- 在栈中为 foo 函数局部变量分配临时空间,若变量有初始化,此时还会赋初值
- push 一些寄存器的值入栈,保存主调函数现场。
【注1:push 寄存器的值,这一操作,可以在分配临时空间之前,也可在其之后,《程序员的自我修养》写的是在开辟临时变量之后】
【注2:教材中,注1操作发生在主调函数栈,且在 foo 函数的参数压入主调函数栈之前;而保存主调函数 ebp 发生在被调函数栈,即在设置被调函数 ebp 之后,由于参考博客更详细,每一步都经得起推敲,操作过程完整,暂以参考博客为准,实际可能两种操作顺序均存在】
在 foo 函数执行完毕后,执行前面阶段的逆操作:
- 保存返回值,通常将函数的返回值保存在寄存器 eax 中。
- pop 一些寄存器的值,即恢复主调函数现场
- 将 esp 值置为当前 ebp,即恢复原栈顶,回收为局部变量分配的临时空间
- 此时栈顶寄存器 esp 所 指向的内存单元 ,即栈顶,存放着之前保存的主调函数 ebp ,将其出栈,赋给 ebp,即销毁被调函数栈,恢复主调函数栈
- 此时栈顶存放着最初入栈的 return address,并跳转到此位置继续执行。
以上就是一个完整的函数调用过程。
再次声明,本文主要内容来自教材,和参考博客 程序的内存布局——函数调用栈的那点事 ,仅做为学习随笔,禁止用于商业用途
网友评论