前言
对于编程这件事来说,函数的调用在代码中是必不可少的,那函数在内存中到底是如何创建和销毁的,下面我将写一个简单的函数为大家演示。
为了更加清楚的理解函数栈帧的创建和销毁,将使用VS2013为大家演示(不同版本的vs演示的效果可能不同),
一 认识寄存器
根据百度百科介绍,寄存器是中央处理器内的组成部分。 寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。 简单来说, 寄存器就是存放东西的 。 从名字来看,跟火车站寄存行李的地方好像是有相似。
在本次函数栈帧的创建和销毁要到寄存器有:
eax
ebx
ecx
edx
esp:栈顶指针
ebp:栈底指针
对于这四个寄存器知道有这些寄存器即可,对于esp和ebp是用来维护函数的。
二 Add函数栈帧的创建和销毁
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int Add(int x, int y) { int c = 0; c = x + y; return c; } int main() { int a = 10; int b = 20; int ret = Add(a, b); printf("ret = %d\n", ret); return 0; }
在调试之前,我们要想一个问题,main函数是程序的入口,但mian函数也是一个函数,那么它又是被什么函数调用的呢?
我们可以调用堆栈进行观察:
这时我们发现main函数是被__tmainCRTStartup函数调用,而__tmainCRTStartup函数又是被mainCRTStartup函数调用。
main函数的调用
int main() { 00881410 push ebp 00881411 mov ebp,esp 00881413 sub esp,0E4h 00881419 push ebx 0088141A push esi 0088141B push edi 0088141C lea edi,[ebp+FFFFFF1Ch] 00881422 mov ecx,39h 00881427 mov eax,0CCCCCCCCh 0088142C rep stos dword ptr es:[edi] int a = 10; 0088142E mov dword ptr [ebp-8],0Ah int b = 20; 00881435 mov dword ptr [ebp-14h],14h int ret = Add(a, b); 0088143C mov eax,dword ptr [ebp-14h] 0088143F push eax 00881440 mov ecx,dword ptr [ebp-8] 00881443 push ecx 00881444 call 008810E1 00881449 add esp,8 0088144C mov dword ptr [ebp-20h],eax printf("ret = %d\n", ret); 0088144F mov esi,esp 00881451 mov eax,dword ptr [ebp-20h] 00881454 push eax printf("ret = %d\n", ret); 00881455 push 885858h 0088145A call dword ptr ds:[00889114h] 00881460 add esp,8 00881463 cmp esi,esp 00881465 call 0088113B return 0; 0088146A xor eax,eax } 0088146C pop edi 0088146D pop esi 0088146E pop ebx 0088146F add esp,0E4h 00881475 cmp ebp,esp 00881477 call 0088113B 0088147C mov esp,ebp 0088147E pop ebp 0088147F ret
这些都是调用main函数的准备工作
其中的push ebp指的是压栈,就是把ebp中的指向的值给ebp。
mov ebp,esp.这里是指把esp中的值给ebp
sub esp,0E4h 是指把esp中的值减去0E4h。
push ebx
push esi
push edi
压栈三个值
lea edi,[ebp-0E4h]
mov ecx,39h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
这些反汇编就是将mian函数的值,从edi开始下面初始化39次,都初始化为CCCCCCCC
所以,为什么有时候会打印出 烫烫烫烫烫烫,因为变量在初始化时内存里面放的CCCCCC的值。
int a = 10;
mov dword ptr [ebp-8],0Ah
这里代码的意思是将a的值放到ptr地址处
mov dword ptr [ebp-8],0Ah
int b = 20;
同理是将b的值放到[ebp-8]的地址处
下面就进行函数的传参,那函数传参又是在内存中进行的呢?
mov eax,dword ptr [ebp-14h]
这里就是将[ebp-14h](b的值)中的值放到eax中
push eax
将eax进行压栈
mov ecx,dword ptr [ebp-8]
这里就是将[ebp-14h](a的值)中的值放到ecx中
push ecx
将ecx进行压栈
下面就进行函数调用
00881444 call 008810E1
为了调用的函数参数能够返回,call指令下一条指令的地址。
下面进入到Add函数中的反汇编。
下面这些又是在干什么呢?其实是在维护Add函数的栈帧。
同main函数的维护一样,都是要函数栈帧的准备工作,分配内存,初始化内存。CCCCCCCCCCCCC
int Add(int x, int y) { 008813C0 push ebp 008813C1 mov ebp,esp 008813C3 sub esp,0CCh 008813C9 push ebx 008813CA push esi 008813CB push edi 008813CC lea edi,[ebp+FFFFFF34h] 008813D2 mov ecx,33h 008813D7 mov eax,0CCCCCCCCh 008813DC rep stos dword ptr es:[edi] int c = 0; 008813DE mov dword ptr [ebp-8],0 c = x + y; 008813E5 mov eax,dword ptr [ebp+8] 008813E8 add eax,dword ptr [ebp+0Ch] 008813EB mov dword ptr [ebp-8],eax return c; 008813EE mov eax,dword ptr [ebp-8]
那函数的值又是怎么返回的呢?形参不是会销毁吗?
mov eax,dword ptr [ebp-8]
这里是将[ebp-8](30)的值保持在eax中。
008813F1 pop edi
008813F2 pop esi
008813F3 pop ebx
008813F4 mov esp,ebp
008813F6 pop ebp
008813F7 ret
那这些又是指的是什么?
pop指的是出栈的意思,使得esp不断加加。
其中的mov esp,ebp是将ebp的值给esp,在 pop ebp 这样esp和ebp维护的空间重新指向main函数。ret是回到函数调用,call指令的下一个地址。
这样我们就是完成了main函数的创建和Add函数创建和销毁。而main函数的销毁和Add函数的销毁是类似的。
下面我们来回答几个问题,来运用我们今天学习的。
1 局部变量是如何创建的?
首先为函数分配好空间,在为函数初始化,最后在为局部变量在分配好的空间中,分配好地址。
2 为什么局部变量不初始化内容是随机的?
因为我们在为函数初始化的时,就是初始化的随机值。
3 函数调用时参数时如何传递的?传参的顺序是怎样的?
将参数存入eax寄存器中,传参的顺序是从右到左。
4函数调用的结果是如何返回的?
通过call指令记住下一条指令的地址,从而找到main函数,在通过寄存器将返回值带到,main函数中。
总结
通过对Add函数创建和销毁的理解,我们相信大家对于函数在内存中的分配会有更加深刻的理解。在下面的博客中,博主会给大家带来更多知识,希望大家不要错过噢!