堆栈在内存中的压栈和弹栈工作原理
一.概述:
网上关于堆栈的文章很多,但多为不祥尽.趁清明假期整理验证下.VC编译,XP平台.
调用函数入栈过程分以下5步: 1.压参数(右向左)-->2.压调用完函数后的第一条汇编指令-->3.保存本函数的栈顶地址-->4.申请子函数的栈空间(会预留一部分空间)-->5.子函数局部变量压栈...其中第1步是在main函数压的栈;第2步是在main函数运行完CALL test这条指令进入test函数时压的栈;第3步是运行完第3步的第一条指令后压的栈;第4-5步也都是在test函数里压的栈.
二.源码:
#include <stdio.h>
#include <string.h>
int test(int iA, int iB)
{
int iC = iA + iB;
printf("test: 0x%p\n",test);
return iC;
}
int main(int argc, const char* argv[])
{
int a = 3;
int b = 4;
printf("main: 0x%p\n",main);
test(a, b);
return 0;
}
三.汇编:
调用main函数前的栈空间初始化,堆初始化和后面main函数的弹栈处理就略过了.我们只看test子函数的过程就足够了.
.
.
.
0040D75E CC int 3
0040D75F CC int 3
--- D:\VC\1\ollydog\ollydog_test.cpp ----------------------------------------------------
1:
2: #include <stdio.h>
3: #include <string.h>
4:
5: int test(int iA, int iB) #压栈,也就是第2步 ,SP指针: 0x0012FF20
6: {
0040D760 55 push ebp #压栈,也就是第3步. SP指针: 0x0012FF1C
0040D761 8B EC mov ebp,esp #把SP指针暂存备份起来.
0040D763 83 EC 44 sub esp,44h #把SP指针往后移动0x44字节. SP指针: 0x0012FF1C - 0x44 = 0x0012FED8
0040D766 53 push ebx #把一些临时变量压栈 SP指针: 0x0012FED4
0040D767 56 push esi #把一些临时变量压栈 SP指针: 0x0012FED0
0040D768 57 push edi #把一些临时变量压栈 SP指针: 0x0012FECC
0040D769 8D 7D BC lea edi,[ebp-44h] #开辟test的栈空间. edi =0x0012FF1C - 0x44 = 0x0012FED8
0040D76C B9 11 00 00 00 mov ecx,11h #计数 ecx = 0x44 / 4 = 0x11 ,准备循环0x11次
0040D771 B8 CC CC CC CC mov eax,0CCCCCCCCh #初始化 eax = 0xCCCCCCCC
0040D776 F3 AB rep stos dword ptr [edi] #将刚才开辟的栈空间都初始化为0xCCCCCCCC.
7: int iC = iA + iB;
0040D778 8B 45 08 mov eax,dword ptr [ebp+8] #eax = 4 ,将0x0012FF1C + 8 的值取出来给eax ,这个形参是在main压的栈.也就是第1步压的栈.
0040D77B 03 45 0C add eax,dword ptr [ebp+0Ch] #eax += 3 ,将0x0012FF1C + C 的值取出来给eax,这个形参是在main压的栈.也就是第1步压的栈.
0040D77E 89 45 FC mov dword ptr [ebp-4],eax #将结果7 压栈.(其实是在test栈空间的有效变量压的栈,也就是形参后面压的栈,而不是栈空间的栈顶(高地址为栈底,低地址为栈顶,sp指针会一直指向栈顶,也就是向低地址发展).)
8: printf("test: 0x%p\n",test);
0040D781 68 05 10 40 00 push offset @ILT+0(test) (00401005) #进入printf库函数的压栈,我们不用关心,相于于在main压int iB 形参的栈
0040D786 68 A4 2F 42 00 push offset string "test: 0x%p\n" (00422fa4) #进入printf库函数的压栈,我们不用关心,相于于在main压int iA 形参的栈
0040D78B E8 50 FF FF FF call printf (0040d6e0) # 调用库函数,相当于进入test函数一个道理
0040D790 83 C4 08 add esp,8 #把刚的给printf函数形参的栈POP出来
9:
10: return iC;
0040D793 8B 45 FC mov eax,dword ptr [ebp-4] #把结果7从栈中取出来组eax,准备返回函数值.
11: }
0040D796 5F POP EDI #把一些临时变量退栈POP完后SP指针: 0x0012FED0
0040D797 5E POP ESI #把一些临时变量退栈POP完后SP指针: 0x0012FED4
0040D798 5B POP EBX #把一些临时变量退栈POP完后SP指针: 0x0012FED8
0040D799 83C4 44 ADD ESP,44 #退第4-5步 ,退完整个栈空间, POP完后SP指针: 0x0012FF1C
0040D79C 3BEC CMP EBP,ESP #系统判断,栈指针和备份栈指针是否相等.相等后才能退出函数.
0040D79E E8 2D39FFFF CALL ollydog.__chkesp #系统检查栈指针SP
0040D7A3 8BE5 MOV ESP,EBP #把备份SP指针给栈指针.
0040D7A5 5D POP EBP #退第3步 POP完后SP指针: 0x0012FF20
0040D7A6 C3 RETN #退第2 步 POP完后SP指针: 0x0012FF24 ,也就是指向了main函数压栈的最后一个形参int iA.
.
.
.
.
.
.
0040105E CC int 3
0040105F CC int 3
--- D:\VC\1\ollydog\ollydog_test.cpp ----------------------------------------------------
12:
13: int main(int argc, const char* argv[]) #main函数开始.
14: {
00401060 55 push ebp
00401061 8B EC mov ebp,esp
00401063 83 EC 48 sub esp,48h
00401066 53 push ebx
00401067 56 push esi
00401068 57 push edi
00401069 8D 7D B8 lea edi,[ebp-48h]
0040106C B9 12 00 00 00 mov ecx,12h
00401071 B8 CC CC CC CC mov eax,0CCCCCCCCh
00401076 F3 AB rep stos dword ptr [edi]
15: int a = 3;
00401078 C7 45 FC 03 00 00 00 mov dword ptr [ebp-4],3
16: int b = 4;
0040107F C7 45 F8 04 00 00 00 mov dword ptr [ebp-8],4
17: printf("main: 0x%p\n",main);
00401086 68 0A 10 40 00 push offset @ILT+5(_main) (0040100a)
0040108B 68 B0 2F 42 00 push offset string "test: 0x%x\n" (00422fb0)
00401090 E8 4B C6 00 00 call printf (0040d6e0)
00401095 83 C4 08 add esp,8
18: test(a, b);
00401098 8B 45 F8 mov eax,dword ptr [ebp-8] #test函数开始
0040109B 50 push eax # 把 b = 4 取出来,当形参准备压栈
0040109C 8B 4D FC mov ecx,dword ptr [ebp-4]
0040109F 51 push ecx # 把 a = 3 取出来,当形参准备压栈
004010A0 E8 60 FF FF FF call @ILT+0(test) (00401005) # 进入test 函数
004010A5 83 C4 08 add esp,8 #test函数结束 把形参int iB ,int iA 退栈
19: return 0;
004010A8 33 C0 xor eax,eax
20: }
004010AA 5F pop edi
004010AB 5E pop esi
.
.
.
四.分析图:
1) VC 堆栈图
函数的压栈顺序结构图: 1.本函数的形参;2.运行后本函数的下一条指令(一般为退形参栈的指令)地址;3.父函数的栈空间栈顶的前一个DWORD;4.本函数内的要用到的栈空间;5.一些临时变量.
玫瑰红框:
0x0012FECC --> 0x0012FED4 临时变量
0x0012FED8 --> 0x0012FF18 test栈空间
0x0012FF1C --> 0x0012FF20 main函数的栈空间栈顶的前一个DWORD 和 运行后本test函数后退int iB ,int iA的退栈指令地址
0x0012FF24 --> 0x0012FF28 test函数的形参,int iA ,int iB
橙色框:
0x0012FF2C --> 0x0012FF34 临时变量
0x0012FF38 --> 0x0012FF7C main栈空间
0x0012FF80 --> 0x0012FF84 main函数的栈空间栈顶的前一个DWORD 和 运行后本test函数后退const char* argv[],int argc的退栈指令地址
0x0012FF88 --> 0x0012FF8C main函数的形参const char* argv[0],int argc
2-0) 通过上分析对应下图的分析图(通过下图就可以很清楚压栈的先后顺序了):
2-1) main 函数的形参
2-2)调用完main函数后的第一条汇编指令:add esp,0Ch.
2-3)调用完test函数后的第一条汇编指令:add esp,8.