目录
笔记下载(1234)
这部分知识有什么用?可以解决什么问题?
前期学习的时候,我们可能有很多困惑
比如:
局部变量是怎么创建的?
为什么局部变量的值是随机值?
函数是怎么传参的?传参的顺序是怎样的?
形参和实参是什么关系?
函数调用是怎么做的?
函数调用是结束后怎么返回的?
函数栈帧是一大块很大的部分,需要一点点的引入。
那么这部分知识就可以用来解决你的疑惑。
专有词太多无法理解怎么办?
本篇主要是记录栈帧创建和销毁的核心思想,对于一些专有词汇不需要过多的了解,只需知道其在这里的功能即可,核心思想是体会汇编语言是如何进行栈帧的创建和删除,包括但不限于函数调用,实参形参,传参,创建销毁等一系列操作。
栈帧的引入
首先我们要明确,计算机内部是有很多结构的,比如内存缓存寄存器等等,其中要引入的就是寄存器。寄存器并不是其他复杂的东西,它就是计算机内部的一个实体化的物件,,而在编译器进行编译的时候会引入寄存器帮助进行编译,常见的寄存器有eax,ebx,ecx,edx,ebp,esp。而在这其中和函数栈帧紧密相关的就是ebp和esp。
ebp和esp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。每个函数在调用的时候都要在栈区创建一个空间,以用来支持函数内部进行变量创建计算销毁等复杂的操作。
我们都知道,一个程序都是从main函数开始的,但是实际上,在编译器内部并非是从main函数开始的,在vs编译器中,通过栈区调用可以知道在调用main函数之前还有__tmainCRTStartup和mainCRTStartup函数,具体可以图示为下面这部分:
编辑
后面会放上相关我个人的笔记,可以下载观看。
那么就可以开始分析整个程序从main函数到传参形参这么一系列操作了。
main函数栈帧的开辟
前面铺垫了那么多,主要为了引出程序是如何开始的,我们以下面这个简单的程序来进行分析:
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); return 0; }
这是一个非常简单的加法程序,在进行调试时,我们可以选择反汇编即可看到各项操作,要说明的是,这些操作是编译器内部进行的,则在不同编译器甚至相同编译器的不同版本中,是不全相同的,但是基本的逻辑是相同的,因此运行出和本篇不同的结果是完全正常的,我这里选择的是vs2013,在这个编译器下的结果较为简单,更容易使人接受基本逻辑。
首先,函数都是从__tmainCRTStartup函数开始的,那么由此就能创建出一个栈。
这是栈的基本框架:
编辑
而esp和ebp只是用来维护函数栈帧的,在此不用过多考虑,只需要知道esp和ebp指向的区域可以理解为栈帧区域。
将程序进行反汇编后,可以看到下面的信息,我将下面信息出发,一段一段补充代码是如何运行的。
编辑
由于代码并非从main函数直接开始,而是有些前期的操作,所以在定义变量前也是有各项操作的
1.栈帧的初始化
1.首先看到的是push ebp,这个操作是压栈(push),相当于把ebp放到了栈顶,并让esp指向新的栈顶。
图示话这一步看=可以画出这样的图:
编辑
在这里要说明,每次进行push操作后,都要让esp--,即让esp指向压栈后的新的栈顶。
2.mov ebp,esp 该操作的意思是把ebp的地址移到esp处(该操作和第三步一起看)
3.sub esp,0E4h 该操作的意思是把esp减去0E4个单位(H是编译器的记录方式,我们只需要看前面的数字即可),那么第二步和第三步的图示为:
编辑
不难看出,把ebp移到esp的位置,再把esp减去对应的数字(该数字是编译器自己决定的) ,这样就能在内存中开辟出一块新的区域,而这块新的空间就是为main函数开辟的栈帧,在这块区域内可以进行各种操作。
4.push ebx/esi/edi
把这三个进行压栈处理,并让esp指向新的栈顶。
编辑
5.lea/mov/mov/rep stos
首先解释什么是lea:load effective address即加载有效地址,它的功能是和后续这些操作一起用以把main函数的栈帧进行初始化。后续这些操作的意思分别为:把39h赋给ecx;把0CCCCCCCCh赋给eax;从edi开始向下ecx个单位的dword全部初始化为eax,可以理解为初始化ecx次,每次初始化dword个内容。其中dword的意思是双字节(double word)。
从这里也可以解决为什么在一开始学习时,打印结果会出现烫烫烫这样的结果,就是因为函数栈帧中的内容被初始化了,而初始化的内容是由编译器自己决定的,如果后续没有对这部分内容进行修改而直接使用输出时,便会导致出现乱码或其他字符。
自此,我们就把main函数栈帧里的内容进行初始化,接下来就进入了正式函数。
2.变量创建和传参过程
变量创建
接下来就真正进入函数内部了,首先是创建int型变量abc:
编辑
这三个变量的指令基本相同,只解释其中一个:
对于变量a:该指令的意思是把0Ah(也就是10)赋给ebp-8这个地址中,而ebp指向的是栈底,那么图示化结果如下:
编辑
可见, 变量在内存中的分布并不是紧密相连的,是由编译器自己决定如何分配内存。
函数传参
首先先看编译器的处理过程:
编辑
看接下来的指令:mov eax ebp-14h意为eax指向ebp-14h这块空间,而事实上这块空间存储的就是变量b,紧接着是push压栈,把b压栈到栈顶;后续a的操作和b相同。
那么这两个操作实际上就是在为后面的Add函数传参,我们后续解释。
编辑
进入函数内部
下一条指令是call,而call的意思是把下一条语句的地址压栈,并让esp指向最新的栈顶,这是什么意思?实际上,这个地址就是用来提前准备一个地方用来接收未来函数传回来的值,等未来函数返回了一个值后,就从call这里回来。在进行call语句后,就进入了函数内部,接下来在函数内部分析。
先放进入函数内部后的指令:
编辑
开辟Add的栈帧
看指令又看到了熟悉的push/mov/sub 没错,这些指令和main函数的意义相同,也是在创建新的一块栈帧,因为我们要调用Add函数了,在调用前创建一块属于它的空间是必须的。
压栈ebx/esi/edi
Add函数初始化
接下来的lea/mov/rep stos和main函数的栈帧处理是一样的,也是将该区域进行初始化,自此我们可以知道,编译器在进行函数处理时,不管是main函数还是其他函数,都会创建出一块属于这块函数自己的栈帧,并且要对之进行初始化,初始化的内容和开辟空间由编译器自己决定。
创建变量接收传参
创建了z用来接收并计算传参,这里指令和main函数创建变量相同。
接下来操作是mov:把ebp+8的值放到eax这个寄存器里面,那么ebp+8是哪里?
编辑
从图示可以看出,实际上ebp+8的值就是我们之前压栈的区域,而后续执行add [ebp+0Ch],0Ch就是12,也就是说把ebp+12这块空间的内容加到eax中;接着mov ebp-8 eax:把eax的值放到eax-8中。自此便完成了这些操作。
这里也解释了为什么说在函数传参的过程中,形参是实参的一份临时拷贝。
栈帧的销毁
返回值
计算出结果就可以往出传参了,其实往出传参就是在销毁Add函数的这块栈帧。
编辑
首先解释pop:pop就是销毁的过程。
mov指令:把ebp-8这个区域内的数值放到eax中,防止销毁时丢失数据。
pop:把数据从栈顶中抽离出去。
mov/pop:其实和栈帧的创建过程相同,把esp和ebp的位置改变就能完成销毁的操作,要注意的是,pop ebp后,ebp找到的是先前存储main函数的位置空间,因此就完成了销毁Add函数的操作,同时也把ebp和esp重新指向了main函数的栈帧,要进行后续操作了。
后续就是传出后,由前面接收到的call指令,继续进行剩余的操作,由此可见整个流程是十分严谨的,在进行数据的处理和栈帧空间的开辟销毁是一个十分严密的过程。
对一些问题的补充:
对于开辟空间位置的选择和开辟空间的大小都是由编译器自己来决定的。
笔记下载
链接:https://pan.baidu.com/s/13VS-rIuuh8ZaER_bOKATgg?pwd=1234
提取码:1234
有不对的地方提出及时改正~~~