C/C++ 栈帧笔记

原创链接

非原创, 有删改,仅做个人笔记


1.我们在调用它的时候系统做了什么?

2.main函数中如果还有另一个函数,在跳转后运行完这个函数时,编译器怎么知道下一行执行哪个语句呢?会不会又从头执行了?

3.函数在结束之后(运行到反花括号“}”处),系统又是怎么处理的?

4.不同的语言对函数形参内存的处理都是一样的吗?

5.函数的返回值有哪些类型?都是怎么从函数中返回回来的呢?

示例代码:

#include <stdio.h>
 
int sum (int a,int b)
{
    int temp = 0;
    temp = a + b;
    return temp;
}
    
int main ()
{
    int a = 10;
    int b = 20;
    int ret = 0;
    ret = sum(a,b);
    printf("ret = %d\n",ret);
    return 0;
}

运行时,从main开始执行。在调用main函数时,首先要做的就是创建main函数的栈帧。也就是说,调用一个函数之前肯定是先给他开辟栈帧,存放函数中的变量以及其它的东西。具体有什么,我们就来看看main函数的栈帧吧

图一
1

这是main函数最开始的样子,其实也可以说是每个函数最开始的样子。ebp和esp是两个寄存器,不过它们都是在保存栈的地址,作用和指针没什么差别,我们就把它们看成指针。其中,ebp是栈底指针,处于高地址处,esp是栈顶指针处于低地址处。在调用了函数从左边的花括号开始,函数就开辟好了自己的空间。esp指向一块区域同时也是栈底

进入main之后继续往下走,是a,b,ret三个局部变量。系统就将它们都存入main函数的栈帧中, 通过ebp减去偏移量的方式来访问内存。

现在,我们将a、b、ret都存入栈了,现在栈长这样:

图二
2

接下来我们继续执行。执行到这一句:ret = sum(a,b);

我们现在要进入sum函数去了,sum 函数有参数, 此时参数以从右向左的顺序依次入栈, 访问的话通过 ebp减去偏移量访问, 执行完 sum 函数需要返回到当前上下文以继续执行, 需要将 sum 函数的下一行代码的地址记录到栈中.

现在的栈如下:

图三
3

该正式地调用sum函数了,对代码进行反汇编可以看到此时执行了一个jmp指令。我们在讲虚拟地址空间的时候说过给函数分配的地址是一个偏移量,偏移量加上pc寄存器的值才能跳转到函数真实的地址。所以在调用call时,先会跳转到jmp表确定函数的地址,再进入函数。

同时,esp发生了变化,向低地址方向移动了。这说明,在跳转到jump这里时,sum函数的下一行指令地址入栈了, 用于回到代码原来位置的。现在的栈如下已经接下来sum函数的汇编代码如下:

图四
在这里插入图片描述

图五
在这里插入图片描述

我们进入到了sum函数里。不过,在往sum函数存它自带的东西之前我们当然要先开辟sum的栈帧啦!和main函数类似,我们就借此机会了解一下栈帧开辟的具体过程。

首先调用push ebp,根据上图我们此时的ebp指向的是main函数的栈底地址。push ebp即将这个地址入栈,这就是便于sum函数执行完毕之后能让ebp回退到main函数栈底的方法。所以这块内存存的就是调用方函数的栈底地址。我们一开始说main函数最底下那块“神秘区域”现在你明白里面存的什么了吗?没错,就是调用main函数的函数(mainCRTStartup)函数的栈底指针,main函数结束后ebp会回退到mainCRTStartUp()函数的栈底!

接下来mov ebp,esp 将esp赋给ebp,即让ebp指向esp指向的那块区域。由于刚才入栈了一次main函数的栈底地址,所以现在栈已经成了这样:

图六
5

接下来进行的操作 sub esp 0CCh 将esp+=0CC 即将栈顶指针上移了0CC,作为sum函数的栈顶。也意味着sum函数栈帧的大小 就是0CC。接下来的对edi,eci,ecx寄存器的操作我们都不用关心,因为它们在这例子里什么都没做,只是入栈又出栈。我们要重点关心的是这两句:

mov eax 0CCCCCCCCh

rep stos dword ptr es:[edi]

这两句的意思是先将CCCCCCCC存进eax,再用rep stos指令(类似循环拷贝)对我们开辟的栈进行初始化赋值。于是栈变成了这样:

图七
图6

这时,sum函数的栈帧就完全申请好了。main函数在创建的时候,也会经历这样一串固定的流程。说句题外话,0CCCCCCCC对应的汉字就是“烫”,所以我们平常如果看见一串“烫烫烫烫…”你就应该明白了,你打印的值指向的是栈中没有人为初始化的部分!(另一种情况是堆中未人为初始化,显示的是“屯”)

接下来就是存入temp这个局部变量了,当然地址是ebp-4。这时候我们的栈帧就是完全状态了!

图八
图7

现在执行sum函数, 将里面的局部变量temp等压入栈中

这时,我们的sum函数已经执行完了,迎来了它生命的终结:“}”反花括号。那么开辟了的栈帧要怎么回退呢?

图九
在这里插入图片描述

我们看到,首先是edi,esi,ebx这三个我们说过的没起作用的寄存器出栈。然后mov esp,ebp即将esp的指向从栈顶直接拉到栈底,放弃栈的空间。此时回到了图六的状态。

然后pop ebp,这行指令的意思是先将此时栈顶的元素赋值给ebp,再将栈顶出栈。那么这时候ebp就回到了main函数的栈底,回归到图五的状态。

接下来是ret指令。这个指令又具体干了什么呢?ret和pop很像,也是将当前栈顶元素(sum下一行指令的地址)赋给一个寄存器然后出栈。赋给谁呢?赋给专门存下一行运行地址的 pc寄存器了。

现在梳理一下状况,ebp已经回到main函数的栈底,esp也由于出栈操作退回到了sum函数的两个实参处,eax寄存器中保存着sum函数的运算结果:temp=a+b,pc寄存器中保存着sum下一行的指令。现在栈的情况是图三.

现在sum函数已然消失,传入的形参a和b自然也应该人去楼空了。编译器执行add esp,8,将esp向高地址移动8位,退还a,b所占的栈帧。此时栈状态是图二.

接下来,我们要将eax中存的计算结果赋给main函数中的ret变量了:

mov dword ptr [ebp-0Ch],eax

至此,sum函数正式结束。接下来就是打印函数了。当打印函数也完成后,我们的main函数也遇到了它的反花括号。重复上述的函数退栈过程,栈底指针回归mainCRTStartUp()中。这便是这份简单的源代码运行的背后的流程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章