目录
1.保存上一个函数(__tmainCRTSARTUP)的栈帧
2.初始化本函数(main)的栈帧,通过esp来初始化ebp
3.对本函数预留的栈空间(包括本函数的临时变量空间)进行初始化为随机值
9.对本函数预留的栈空间(包括本函数的临时变量空间)进行初始化
前言
每一个函数调用都要在栈区调用一个空间。在函数调用时,用来保存函数内部的临时数据如参数、返回地址等的内存单元就是栈帧。
栈是由高地址向低地址生长的,且栈有栈顶和栈底之分,为了更方便的使用函数内部的临时数据,引入了ebp和esp,寄存器ebp保存的是栈底地址(基地址),esp保存的是栈顶地址。
在函数调用后进入函数内部代码后通过esp来初始化ebp,这样以ebp为基地址的数据,向上为函数的返回值,参数等,向下为调用函数的局部变量等。(总结会讲到)使用ebp方便地将本函数中的栈分成了两部分,我们把ebp和esp之间的数据就称为函数栈帧。这两个地址是用来维护栈帧的,且一次只能维护一个函数。
由于栈帧是与函数的调用位置有关系,并且是用完即还,并且同一个函数在不同的位置调用所形成的栈帧并不相同,故当函数调用完毕,需要回收它使用的栈空间,否则引起栈平衡错误,引发程序故障。
栈在使用上一般会通过push或pop指令来进行入栈和出栈,另外一些特殊的指令call和ret,也会进行入栈和出栈操作。
函数的调用栈帧过程的常用指令
1.push:把一个32位的操作数压入堆栈中,这个操作在32位机中会使得esp被减4(字节)
push ecx 等价于 esp=esp-4
2.pop:与push相反,esp每次加4(字节),一个数据出栈。pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中;
pop eax 等价于 esp=esp+4
3. call:调用函数,先压栈call指令的下一条指令,然后跳转到Add函数的地方
4.ret:跳转到调用函数的地方。对应于call,返回到对应的call调用的下一条指令,若有返回 值,则放入eax中
5.xor:异或指令,这本身是一个逻辑运算指令,但在汇编指令中通常会见到它被用来实现清零 功能。用 xor eax,eax这种操作来实 现 mov eax,0,可以使速度更快,占用字节数更少。 6.lea:取得第二个参数地址后放入到前面的寄存器(第一个参数)中。
然而lea也同样可以实现mov的操作,例如: lea edi,[ebx-0ch]
7.add:加法指令,第一个是目标操作数,第二个是源操作数,格式为:目标操作数 = 目标操作数 + 源操作数;
8.sub:减法指令,第一个是目标操作数,第二个是源操作数,格式为:目标操作数 = 目标操作数- 源操作数;
9.mov:数据传送。第一个参数是目的操作数,第二个参数是源操作数,就是把源操作数拷贝到目的操作数一份。
实例讲解函数栈帧的创建和销毁
首先,我们以Add函数为例进行讲解。
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 10;
int ret=Add(a,b);
return 0;
}
109: int Add(int x, int y)
110: {
003D3CD0 push ebp //保留上一个函数(main)的栈帧
003D3CD1 mov ebp,esp // 通过esp,更新ebp
//对本函数预留的栈空间进行初始化
003D3CD3 sub esp,0CCh
003D3CD9 push ebx
003D3CDA push esi
003D3CDB push edi
003D3CDC lea edi,[ebp-0CCh]
003D3CE2 mov ecx,33h
003D3CE7 mov eax,0CCCCCCCCh
003D3CEC rep stos dword ptr es:[edi]
111: int z = 0;
003D3CEE mov dword ptr [z],0 //创建临时变量
112: z = x + y;
003D3CF5 mov eax,dword ptr [x] // 将x保存在eax中
003D3CF8 add eax,dword ptr [y] //将x+y相加
003D3CFB mov dword ptr [z],eax //将相加的值放入Z中
113: return z;
003D3CFE mov eax,dword ptr [z] //把z存储在eax中
114: }
003D3D01 pop edi //edi出栈,esp+4;
003D3D02 pop esi //esi出栈,esp+4;
003D3D03 pop ebx //ebx出栈,esp+4;
003D3D04 mov esp,ebp //回收Add的战争
003D3D06 pop ebp //ebp出栈,将出栈的内容保存到ebp
003D3D07 ret //ret指令会使得出栈一次,跳转到之前函数保留的返回地址
115: int main()
116: {
003D1AE0 push ebp //保存上一个函数的栈帧,将上一个函数栈底的地址压栈
003D1AE1 mov ebp,esp //通过esp,更新ebp
003D1AE3 sub esp,0E4h //开辟0E4h局部变量的空间
003D1AE9 push ebx //保存ebx寄存器的值
003D1AEA push esi //保存esi寄存器的值
003D1AEB push edi //保存edi寄存器的值
003D1AEC lea edi,[ebp-0E4h]
//将ebp-0E4h的值,赋给edi,是所开辟局部变量空间的最低地址
003D1AF2 mov ecx,39h //将39h放ecx寄存器
003D1AF7 mov eax,0CCCCCCCCh //将0xcccccccc的值放进eax中
003D1AFC rep stos dword ptr es:[edi] //初始化本函数(main)的栈帧
117: int a = 10;
003D1AFE mov dword ptr [ebp-8],0Ah //为局部变量a开辟空间
118: int b = 10;
003D1B05 mov dword ptr [ebp-14h],0Ah // 为局部变量b开辟空间
119: int ret=Add(a,b);
003D1B0C mov eax,dword ptr [b] //参数b放入寄存器eax中
003D1B0F push eax //对eax进行压栈
003D1B10 mov ecx,dword ptr [a] //参数a放入寄存器eax中
003D1B13 push ecx //对ecx进行压栈
003D1B14 call _Add (03D11E0h) //调用函数Add,并且对下一条指令进行压栈
003D1B19 add esp,8 //回收形参
003D1B1C mov dword ptr [ebp-20h],eax //将之前Z的值保存到[ebp-20h]
120: return 0;
003D1B1F xor eax,eax //相当于 mov eax,0,实现清零功能
//采取相同方法对main函数回收
121: }
003D1B21 pop edi
003D1B22 pop esi
003D1B23 pop ebx
003D1B24 add esp,0E4h
121: }
003D1B2A cmp ebp,esp
003D1B2C call __RTC_CheckEsp (03D1136h)
003D1B31 mov esp,ebp
003D1B33 pop ebp
003D1B34 ret
1.保存上一个函数(__tmainCRTSARTUP)的栈帧
其实main()函数是在_tmainCRTStartup函数中调用的,第一步压栈保存了调用main函数的__tmainCRTSARTUP的栈帧。
2.初始化本函数(main)的栈帧,通过esp来初始化ebp
将当前esp指向的地址(即存储old ebp值的地址)赋给ebp,这样就能通过esp来初始化ebp,使得ebp的值得以更新,
3.对本函数预留的栈空间(包括本函数的临时变量空间)进行初始化为随机值
1)
2)
3)lea 加载有效地址,把[ebp-0E4h] 这个地址加载到edi中,相当于给edi 放了个地址,这四步相当于 把从edi加载的地址开始向下的39h个dword(四个字节)初始化为0cccccccch。
换句话说就是用eax中的值初始化地址为edi的内存,dword为双字型,一个字为两个字节,双字为4个字节,每执行一次,edi+4,ecx-1,直到ecx=0为止.
4.为局部变量开辟空间
5.函数传参(从右向左)
6.调用函数(call Add)
call操作,相当于push call的下一条指令,然后esp-4
call指令先将函数返回地址压入栈中,再跳转到相应地址执行,此处的返回地址应为call指令的下一条指令地址.
7—10步骤和1—4相同,都是调用函数时的操作,不再赘述
7.保存上一个函数(main)的栈帧
8.初始化本函数(Add)的栈帧
9.对本函数预留的栈空间(包括本函数的临时变量空间)进行初始化
10.为局部变量开辟空间
11.函数栈帧的销毁
回收Add函数内存空间。
采用相同的方法回收mian和__tmainCRTSARTUP
总结:
通过以上的学习,我们对函数栈帧的创建和销毁的理解就更加的透彻,也可以解决之前很多的疑惑,我们通过回答以下几个问题来巩固一下知识。
-
局部变量是怎么创建的?
我们对函数预留的栈空间进行初始化后会为局部变量开辟空间,由此创建了局部变量。
-
为什么我们如果不初始化局部变量,局部变量的值为随机值呢?
我们在创建函数栈帧时,对edi到ebp之间的空间进行了初始化,值为随机值。如果你对局部变量初始化了,那就把随机值覆盖了。
-
函数是怎么传参的?传参的顺序是怎样的?
我们在调用函数之前,就已经把实参放进寄存器中,并且从右向左开始压栈,当我们真正调入函数时,我们可以通过ebp的偏移量来找到形参。
如图所示:ebp+4=函数的返回地址;ebp+8=函数的第一个参数;ebp+12=函数的第二个参数。我在前言也说过,以ebp为基地址的数据,向上为函数的返回地址,参数等。向下为调用函数的局部变量等。所以,可通过基地址方便的寻址函数的局部变量,函数的返回值,函数参数等。
-
形参和实参的关系是什么?
形参是实参的一份临时拷贝,它们值是相等的,但是空间是完全独立的。
-
函数调用是怎么做的?
函数调用通过call指令,先压栈call指令的下一条指令,然后跳转到你所调用的函数的地方。
-
函数调用结束后是如何返回的?
我们在调用函数之前就为自己找好了后路,我们对call指令的下一条指令和上一个函数的ebp进行了压栈保存了下来,当函数调用结束后我们pop了ebp找到了上个函数的栈底,并且ret出栈,返回到对应的call调用的下一条指令,若有返回值,则放入eax中带回。
点个赞再走呗~~~