函数栈帧的创建和销毁在不同编译器的环境下,原理大体是相同的。
eax | |
ebx | |
ecx | |
edx | |
以下两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的 | |
ebp | 栈底指针 |
esp | 栈顶指针 |
每一个函数调用,都要在栈区创建一块空间
我们写如下代码作为示例:
- 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;
- }
执行了上面的代码之后,我们的栈区空间中就会为我们的main开辟一块空间,其中ebp用来记录我们main所占空间的高地址,esp用来记录我们main空间所占的低地址
栈区的使用习惯是先使用高地址,然后使用低地址,也就是先使用我们下面的空间,再使用上面的空间
在VS2013中,main函数也是被其他函数调用的:
mainCRTStartup 调用了 __tmainCRTStartup 调用了main函数
压栈是从栈顶放入一个元素push
出栈是从栈顶弹出一个元素pop
在我们的main函数之前,我们的 __tmainCRTStartup 函数已经在栈区了,我们的ebp和esp指针分别记录 __tmainCRTStartup函数的高地址和低地址(这张图的下面是高地址,上面是低地址)
首先我们执行压栈操作:push ebp,将我们的ebp压入我们栈顶,此时我们的ebp中存储的是我们 __tmainCRTStartup的高地址
当我们ebp入栈之后,我们的栈顶指针esp同时也随之上移
mov ebp,esp
将esp的值赋给ebp,也就相当于将ebp的指针上移到下图的情况
sub esp,0E4h
将esp减去0E4h,0E4h是一个八进制数字228
也就相当于我们esp指针上移,如下图所示,其中我们发现我们的esp和ebp指针已经不再维护原来的__tmainCRTStartup空间,此时那块紫色的空间其实是为了给我们的main函数所开辟的空间
push ebx
push esi
push edi
依次将我们的ebx,esi,edi三个压入栈中,但是同时,我们需要注意到我们的栈顶指针esp会随着每一次入栈,及时地指向栈顶的空间
lea edi,[ebp-0E4h]
lea是load effective address,就是加载有效地址的意思。所以上面的代码就是将后面的这个有效的地址加载到edi里面去
这里我们给ebp减去了0E4h,注意,我们这里的0E4h在之前就已经出现过了,是我们的main函数栈帧的空间,给我们的ebp减去0E4h就是往上移动到了main函数栈帧的顶部
mov ecx,39h
将39h放入ecx中
mov eax,0CCCCCCCCh
将0CCCCCCCCh放入eax中
rep stos dword ptr es:[edi]
将刚刚从edi开始的39h次的dword(double word双字,四个字节)的数据全部都改为0CCCCCCCCh,也就是将我们刚刚为main函数开辟好的内容全部都改成CCCCCCCC
int a=10;
mov dword ptr [ebp-8]0Ah
将0Ah这个十六进制数字,也就是十进制中的10放到ebp-8的位置
int b=20;
dword ptr [ebp-14h],14h
int c=0;
dword ptr [ebp-20h],0
同理
c=Add(a,b);
mov eax,dword ptr [ebp-14h]
将ebp-14h,也就是b的值放到eax中去,也就是将20放入eax中
push eax
将eax压入栈中
mov ecx,dword ptr [ebp-8]
将ebp-8中的值放入ecx中,也就是将我们的a,10,放入ecx中
push ecx
将ecx入栈
与上面一步同理,都是函数的传参
call 00C210E1
调用函数(call的地址为00C2144B),并且会将call指令的下一条指令的地址压入栈中。
这里的意思就是call指令执行之后会跳到函数的地址,当从函数的地址处跳回来的时候,直接执行call的下一条指令
接下来我们就进入了add函数内部
下面的代码和我们的main函数创建的时候非常类似
push ebp
将ebp压入栈中
mov ebp,esp
将esp的值赋给ebp
也就是说我们的ebp指向了esp的位置
sub esp,0CCh
给sub减去0CCh
也就是给我们的add函数开辟出来的空间
push ebx
push esi
push edi
分别将ebx,esi,edi压入栈中
lea edi,[ebp+FFFFFF34h]
加载有效地址,将ebp+FFFFFF34h的地址加载到我们的edi中
mov ecx,33h
将33h存入我们的ecx中
mov eax,0CCCCCCCCh
将0CCCCCCCCh赋值给eax这个寄存器
rep stos dword ptr es:[edi]
让我们把从edi这个位置开始向下到ebp中间全部赋值为0CCCCCCCCh
int z=0;
mov dword ptr [ebp-8],0
将0放入ebp-8的位置,也就是我们的z
mov eax,dword ptr [ebp+8]
将ebp+8的值放入eax中,也就是我们的a
add eax,dword ptr [ebp+0Ch]
将我们ebp+0Ch处的值加到我们的eax中去,也就是将我们的b加给a,这是,我们的eax为30
dword ptr [ebp-8],eax
加完之后,再将我们eax的值放入我们ebp-8的位置,也就是我们的z的地址
这里我们可以注意到我们压栈是先将b压入,再压a,所以我们传参是从右向左传的
也就是我们的add(a,b)是先将b压入,再将a压入的
所以形参并不是我们在add函数内部创建的,而是在传参产生之后回去找到传参时的参数的
return z;
mov eax,dword ptr [ebp-8]
所以这里return z 的命令,也就是将我们的ebp-8的值放入我们的eax中,也就是现将我们的30放入我们的eax中,因为z一会儿出去之后就会被销毁,所以我们需要把我们计算的结果放入我们的寄存器中进行存储。()
pop edi
将栈顶的元素弹出放入我们的edi寄存器中,此时栈顶原本的数据就是edi,也就是说只是将edi的数据读取而已,下面两个也是如此
pop esi
pop ebx
mov esp,ebp
将ebp赋值给esp,也就是说我们的esp指向了与我们ebp相同的位置
pop ebp
将栈顶的元素弹出赋值给ebp,也就是我们原本的ebp-main的位置也就是说现在我们的ebp和esp重新维护我们的main函数的空间。
ret
之前我们将call指令的下一条指令的地址给存了起来,现在我们ret一下,我们现在又成功找到了我们之前call指令的下一条指令
add esp,8
esp+8也就是让我们的栈顶指针下移动到我们b的下面,也就是将我们之前那两个已经使用过的形参x,y的空间销毁
mov dword ptr [ebp-20h],eax
将eax的值放入ebp-20h的位置,也就是给我们的c赋值
至此,我们add函数栈帧的创建和销毁就完成了