目录
引言:
1,函数栈帧的概念
2,函数栈帧的创建与销毁过程
2.1预备知识
2.2main函数栈帧的创建
2.2.1push ebp
2.2.2mov ebp,esp
2.2.3sub esp,0E4h
2.2.4push ebx ;push esi;push edi
2.2.5lea edi,[ebp+FFFFFF1Ch]
2.2.6mov ecx,39h
2.2.7mov eax,0CCCCCCCCh
2.2.8rep stos dword ptr es:[edi]
2.3局部变量的创建
2.3.1mov dword ptr [ebp-8],0Ah
2.3.2mov dword ptr [ebp-14h],14h
2.3.3mov dword ptr [ebp-20h],0
2.4Add函数的调用
2.4.1mov eax,dword ptr [ebp-14h]
2.4.2push eax
2.4.3mov ecx,dword ptr [ebp-8]
2.4.4push ecx
2.4.5call 00B810E1
2.4.6Add函数栈帧的创建
2.4.6.1push ebp
2.4.6.2mov ebp,esp
2.4.6.3sub esp,0CCh
2.4.7依次执行push ebx ;push esi ;push edi
2.4.8lea edi,[ebp+FFFFFF34h]
2.4.9依次执行mov ecx,33h;mov eax,0CCCCCCCCh
2.4.10rep stos dword ptr es:[edi]
2.4.11mov dword ptr [ebp-8],0
2.4.12mov eax,dword ptr [ebp+8]
2.4.13add eax,dword ptr [ebp+0Ch]
2.4.14mov dword ptr [ebp-8],eax
2.4.15mov eax,dword ptr [ebp-8]
2.5函数栈帧的销毁
2.5.1依次执行pop edi;pop esi;pop ebx
2.5.2mov esp,ebp
2.5.3pop ebp
2.5.4ret
2.5.5add esp,8 esp
2.5.6mov dword ptr [ebp-20h],eax
结语:
1.局部变量是怎么创建的?
2.为什么局部变量的值是随机值?
3.函数是怎么传参的?传参的顺序是怎样的?
4.形参和实参是什么关系?
5.函数调用是怎么做的?
6.函数调用结束后是怎么返回的?
引言:
很多伙伴在学习C语言时侯,可能会有很多的困惑?
比如:
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机值?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用结束后是怎么返回的?
其实我们只要知道了函数栈帧的创建和销毁上述这些问题自然就迎刃而解了,学习这部分知识就相当于在修炼我们的内功,让自己理解知识的能力更上一层楼。
注:在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现(本文基于VS2013编译器调试)。
1,函数栈帧的概念
函数栈帧是指在函数调用时,为该函数分配的一块内存区域,用于存储函数的局部变量,参数和其它相关信息。每个函数调用都会创建一个新的函数栈帧,函数执行完毕后,函数栈帧会被销毁。
2,函数栈帧的创建与销毁过程
🎈演示代码:
#include <stdio.h>int Add(int x, int y)
{int z = 0;z = x + y;return z;
}int main()
{int a = 10;int b = 20;int c = 0;c = Add(a, b);printf("%d\n", c);return 0;
}
2.1预备知识
我们知道,每一个函数调用,都要在栈区创建一个空间,假设我们现在调用main函数,就要在栈区内存上面为main函数分配一块空间,我们就称这块空间是为main函数开辟的一块函数栈帧。那这块空间是由谁来维护的呢?它是由两个寄存器来维护的,一个是ebp(栈底指针),一个是esp(栈顶指针)。那我们现在就可以这样理解,正在调用哪个函数,那esp和ebp维护的就是哪个函数的函数栈帧,esp和ebp之间的空间就是为这个函数调用所分配的空间。
接下来,我们执行main函数,鼠标点击工具栏中的调试,窗口,调用堆栈,我们就可以从函数的调用堆栈里边看见main函数被调用了。
到这儿,大家可能会有点困惑,main函数被谁调用了呢?这时候我们让代码继续往下走,当这个代码执行完的时候,我们就可以看见是__tmainCRTStartup() 这个函数内部调用了我们的main函数,而__tmainCRTStartup() 这个函数又被mainCRTStartup()这个函数调用了。由此,我们可以得知,在VS2013中,main函数也是被其他函数调用的。
我们知道,在使用内存的时候,每一次函数调用都要在栈区上分配空间。那我们接着分析这段代码,开始调用main函数后,就为main函数分配了栈帧,调用完后接着往下走,马上调用了Add函数,这时候也要为Add函数分配栈帧,分配完后esp指针和ebp指针就开始维护Add函数的函数栈帧。我们还应该清楚的一点是,在main函数调用之前它的下面也为__tmainCRTStartup()函数和mainCRTStartup()函数分配了空间。
2.2main函数栈帧的创建
接下来我们研究一下这个函数具体是怎么调用的。
第一步,按F10调试,然后右击鼠标点击转到反汇编,这个时候就可以看到C语言所对应的汇编代码了,因为我们一会儿要看具体的地址,内存的布局,而现在显示的是'a','b','c'这样的符号名,不好观察,所以我们可以把显示符号名这个选项卡去掉,去掉后里边就变成了诸如[ebp - 8]这样的地址,方便我们更好的观察。
那这些代码又该如何去理解呢?其实很简单,接下来,跟着博主的思路继续往下走。
现在我们调用main函数,前面说过,再调用main函数之前,还调用了一个函数__tmainCRTStartup(),而现在我们已经进到main函数,马上要调用main函数了,那另外一个调用main函数的那个函数的函数栈帧肯定已经创建好了,现在进入main函数,进去后的指令执行步骤如下:
2.2.1push ebp
push叫做压栈操作,就是把ebp压到__tmainCRTStartup()函数的函数栈帧上边,因为esp总是维护的栈顶,所以,当push完后,esp会指向刚才压进去的ebp。
2.2.2mov ebp,esp
这步操作是将esp的值赋给ebp,那栈底的ebp此时也会指向刚才压进去的ebp。
2.2.3sub esp,0E4h
这步的操作是给esp的值减去十六进制的0E4h,esp的值就会变小,这意味着此时的esp不再指向刚才压进去的ebp,而是指向了上边的某一块区域,此时,就会为main函数预开辟一块空间。
2.2.4push ebx ;push esi;push edi
这三条指令会将ebx,esi,edi依次压到栈顶,而push操作每执行一次,esp的值就会发生变化,等这三条指令执行完毕后,esp就会指向栈顶,即edi的位置。
2.2.5lea edi,[ebp+FFFFFF1Ch]
这条指令的意思是将[ebp+FFFFFF1Ch]加载到edi里边去,相当于给edi里边放了一个地址。
2.2.6mov ecx,39h
这条指令将39h的值放到ecx这个寄存器里边。
2.2.7mov eax,0CCCCCCCCh
这一步是将CCCCCCCC这样的值放到eax里边。
2.2.8rep stos dword ptr es:[edi]
真正产生效果的是这条指令,意思是要把从刚刚edi这个位置开始向下的39h这么多的空间全部都改成CCCCCCCC这样的内容,也就是将刚刚为main函数预开辟的空间里边的内容全部初始化成CCCCCCCC这样的内容。
当我们执行完以上这些指令以后,为main函数栈帧的开辟就已经准备完了。
2.3局部变量的创建
2.3.1mov dword ptr [ebp-8],0Ah
意思是把0Ah这个十六进制数字放在[ebp-8]的位置,0A就是十进制的10,ebp-8的位置就是为a变量开辟的一块空间,由此我们也知道了为什么定义变量的时候要给它初始化,因为如果不初始化,它里边放的就是CCCCCCCC这样的随机值。
2.3.2mov dword ptr [ebp-14h],14h
意思是将十六进制的14h放在ebp-14h的位置上,即为b变量开辟了一块空间,里边放的是十进制的20。
2.3.3mov dword ptr [ebp-20h],0
意思是将0放在ebp-20h的位置上,即为c变量开辟一块空间,里边放的是十进制的0。
2.4Add函数的调用
我们知道函数调用就要传参,那在这儿它是怎么传参的呢?我们一起来看一看。
2.4.1mov eax,dword ptr [ebp-14h]
意思是把ebp-14h的值放到eax里边去,而在上边我们分析过[ebp-14h]的位置就是b变量的位置,b变量里边存储的是十进制的20,所以就是把十进制的20放到eax里边去。
2.4.2push eax
push即压栈,就是把eax压到栈顶上去,而eax里边放的是20,即把20压栈到了栈顶上去。此时esp指向了eax的位置。
2.4.3mov ecx,dword ptr [ebp-8]
意思是把ebp-8的值放到ecx里边去,而上边我们也分析过ebp-8的位置就是a变量的位置,a变量里边存储的是十进制的10,所以就是把十进制的10放到ecx里边去。
2.4.4push ecx
意思是把ecx里边的值压栈到栈顶上去,即把10压倒了栈顶,此时esp指向了ecx的位置。
2.4.5call 00B810E1
call指令是调用的意思,此时,我们按F11进入到它的内部,可以看到它把call指令的下一条指令的地址压到了栈顶,为什么要记住这个地址呢,我们可以想象一下,call指令一调用就会马上去调用add函数,而add函数调用完后它要回到call指令的下一条指令继续往下执行,所以add函数回来的时候就会找到这个地址,再从这个地址继续往下执行。
执行到这的时候我们在按F11来到真正的Add函数内部:
2.4.6Add函数栈帧的创建
可以看到前面的汇编代码和main函数内部的一模一样,这是在为Add函数创建函数栈帧。
2.4.6.1push ebp
意思是将main函数栈帧底部的ebp压到栈顶,此时esp指向栈顶的ebp。
2.4.6.2mov ebp,esp
将esp的值赋给ebp,此时,ebp也指向了和esp同样的位置。
2.4.6.3sub esp,0CCh
给esp减去0CCh,此时,esp就会指向上边的某一块区域,这时,Add函数的函数栈帧也创建好了。
2.4.7依次执行push ebx ;push esi ;push edi
这三条指令会将ebx ,esi ,edi依次压倒栈顶,此时esp指向了栈顶的edi。
2.4.8lea edi,[ebp+FFFFFF34h]
将ebp+FFFFFF34h这个地址加载到edi里边去。
2.4.9依次执行mov ecx,33h;mov eax,0CCCCCCCCh
分别将33h和0CCCCCCCCh 的值放到ecx和eax里边去。
2.4.10rep stos dword ptr es:[edi]
将从edi这个位置开始向下到ebp之间这所有的空间的内容初始化成CCCCCCCC。
🍂int z = 0;
2.4.11mov dword ptr [ebp-8],0
把0放到ebp-8的位置上去,即给z开辟一块空间,ebp-8指向的就是z这块空间。
🍂z = x + y;
2.4.12mov eax,dword ptr [ebp+8]
把ebp+8的值放到eax寄存器里边去,而ebp+8指向的这块空间刚好是ecx,它里边放着的是10,所以,eax寄存器里边放着的就是10。
2.4.13add eax,dword ptr [ebp+0Ch]
给eax里边加上ebp+0Ch的值,而ebp+0Ch指向的是eax,它里边放着的是20,所以,eax现在就变成了30。
2.4.14mov dword ptr [ebp-8],eax
把eax的值放在ebp-8的位置上去,ebp-8即z空间,所以,现在z里边放的就是30。
🍂return z;
2.4.15mov eax,dword ptr [ebp-8]
把ebp-8的值放在eax里边去(eax是寄存器,它不会因为程序退出就销毁里边的值),ebp-8就是z空间,里边放的是30,所以就是把30放在eax寄存器里边去。
2.5函数栈帧的销毁
2.5.1依次执行pop edi;pop esi;pop ebx
pop是出栈操作,意思是依次将edi,esi,ebx 弹出栈顶。
2.5.2mov esp,ebp
把ebp赋给esp,此时esp不在指向栈顶,而是指向和ebp同样的位置。
2.5.3pop ebp
此时ebp和esp都指向了栈顶,而这步操作就是把ebp弹出,而ebp弹出后esp也不应该指向这儿,而是向下移一个位置,而移动后esp刚好就指向了前面call指令记住的地址。
2.5.4ret
这条指令执行后刚好就会回到call指令的下一条指令的地方,退出Add函数,回到main函数里边。
回到main函数里边后,main函数的函数栈帧又是由esp和ebp来开始维护。
2.5.5add esp,8 esp
这是esp指向栈顶,加上8就会向下移动到指向edi的位置,相当于把形参x,y还给操作系统。
2.5.6mov dword ptr [ebp-20h],eax
把eax的值放到ebp-20h的位置上,eax寄存器里边放的是30,ebp-20h的位置即c变量的位置,也就是将30赋给c变量。
结语:
现在我们就可以解答文章开头的疑惑了:
1.局部变量是怎么创建的?
局部变量的创建是首先为这个函数分配好栈帧空间,栈帧空间里边初始化好一部分空间之后给局部变量在栈帧里边分配一点空间,这就是局部变量的创建。
2.为什么局部变量的值是随机值?
因为如果不初始化的话,里边的值是我们放进去的,所以就是随机值,如果给局部变量初始化的话,就会把这个随机值给覆盖了。
3.函数是怎么传参的?传参的顺序是怎样的?
当我们要去调用函数的时候,还没有调用的时候就已经用push指令把这两个参数从右向左开始压栈进去了,当真正进入形参函数的时候,在Add函数栈帧里边通过指针的偏移量找回来找到了我们的形参。
4.形参和实参是什么关系?
形参是在压栈的时候开辟的空间,它和实参只是值是相同的,但空间是独立的,所以形参是实参的一份零时拷贝,改变形参不会影响实参。
5.函数调用是怎么做的?
见上文
6.函数调用结束后是怎么返回的?
函数在调用之前就把call指令的下一条指令的地址存进去了(即把ebp调用这个函数的上一个函数的栈帧的ebp存进去),当函数调用完要返回的时候,弹出ebp就能够找到上一个函数调用的ebp,然后指针往下走的时候就能够找到esp的地址,回到我们的栈帧空间,返回值是通过寄存器的方式带回来的。