目录
本篇文章采用vs2022,在x86环境下运行。
寄存器介绍:
首先介绍一下寄存器:eax,ebx,ecx,edx,esp,ebp。
其中esp成为栈顶指针,ebp为栈底指针,他们共同维护一块栈帧空间。
而eax,ebx,ecx,edx则是保存变量。
下面我们先讲下大概:
首先我们要知道每个函数再被调用时都会在栈区开辟一段空间。
查看调用堆栈,我们发现,main函数也被其他函数调用。
下面具体展示一下main函数的栈帧时如何创建的。
main函数在被调用前,调用main函数的exeinvoke_main()函数栈帧已经创建完毕,由ebp,esp共同维护。
接着我们转到反汇编。
002618B0 push ebp
002618B1 mov ebp,esp
002618B3 sub esp,0E4h
002618B9 push ebx
002618BA push esi
002618BB push edi
002618BC lea edi,[ebp-24h]
002618BF mov ecx,9
002618C4 mov eax,0CCCCCCCCh
002618C9 rep stos dword ptr es:[edi]
002618CB mov ecx,26C008h
002618D0 call 0026131B
int x = 10, y = 20;
002618D5 mov dword ptr [ebp-8],0Ah
002618DC mov dword ptr [ebp-14h],14h
int c = 0;
002618E3 mov dword ptr [ebp-20h],0
c = add(x, y);
002618EA mov eax,dword ptr [ebp-14h]
002618ED push eax
002618EE mov ecx,dword ptr [ebp-8]
002618F1 push ecx
002618F2 call 00261023
002618F7 add esp,8
002618FA mov dword ptr [ebp-20h],eax
栈帧的创建
001218B0 push ebp //插入ebp,压栈
001218B1 mov ebp,esp //把esp的值给ebp
001218B3 sub esp,0E4h //esp-oe4h
初始化
001218BC lea edi,[ebp-24h] //把[ebp-24h] 的值给edi
001218BF mov ecx,9
001218C4 mov eax,0CCCCCCCCh
001218C9 rep stos dword ptr es:[edi] //从edi开始进行ecx次初始化值0CCCCCCCCh
至此,函数栈帧创建并初始化完毕,这也就是为什么在某些情况下会打印出烫烫烫烫烫。
函数临时变量的创建
int x = 10, y = 20;
001218D5 mov dword ptr [ebp-8],0Ah //把0ah值给ebp-8位置
001218DC mov dword ptr [ebp-14h],14h //把14h值给ebp-14位置
int c = 0;
002618E3 mov dword ptr [ebp-20h],0 //把0给ebp-20h位置
至此,函数临时变量创建完毕。
函数的调用
c = add(x, y);
002618EA mov eax,dword ptr [ebp-14h] //把ebp-14h位置的值赋给eax,也就是y的值
002618ED push eax //压栈
002618EE mov ecx,dword ptr [ebp-8] //把ebp-8为位置的值给ecx,也就是x的值
002618F1 push ecx //压栈
002618F2 call 00261023 //call指令调用函数,会把call指令下一条指令的地址压栈
002618F7 add esp,8
002618FA mov dword ptr [ebp-20h],eax
形参的传递
c = add(x, y);
002618EA mov eax,dword ptr [ebp-14h] //把ebp-14h位置的值赋给eax,也就是y的值
002618ED push eax //压栈
002618EE mov ecx,dword ptr [ebp-8] //把ebp-8为位置的值给ecx,也就是x的值
002618F1 push ecx //压栈
我们从中发现,形参的传递是从右往左传递,形参与实参所处空间不同,所以形参的改变不能改变实参。
002618F2 call 00261023 //call指令调用函数,会把call指令下一条指令的地址压栈
002618F7 add esp,8
002618FA mov dword ptr [ebp-20h],eax
call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈
操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。
下面进入add函数:
00261850 push ebp
00261851 mov ebp,esp
00261853 sub esp,0CCh
00261859 push ebx
0026185A push esi
0026185B push edi
0026185C lea edi,[ebp-0Ch]
0026185F mov ecx,3
00261864 mov eax,0CCCCCCCCh
00261869 rep stos dword ptr es:[edi]
0026186B mov ecx,26C008h
00261870 call 0026131B
int z = 0;
00261875 mov dword ptr [ebp-8],0
z = x + y;
0026187C mov eax,dword ptr [ebp+8]
0026187F add eax,dword ptr [ebp+0Ch]
00261882 mov dword ptr [ebp-8],eax
return z;
00261885 mov eax,dword ptr [ebp-8]
add函数栈帧的创建与main函数同理。
00261850 push ebp
00261851 mov ebp,esp
00261853 sub esp,0CCh
00261859 push ebx
0026185A push esi
0026185B push edi
0026185C lea edi,[ebp-0Ch]
0026185F mov ecx,3
00261864 mov eax,0CCCCCCCCh
00261869 rep stos dword ptr es:[edi]
下面进行运算:
int z = 0;
00261875 mov dword ptr [ebp-8],0
z = x + y;
0026187C mov eax,dword ptr [ebp+8] //把x‘放入eax
0026187F add eax,dword ptr [ebp+0Ch] //把y’加入eax
00261882 mov dword ptr [ebp-8],eax //把eax值放入ebp-8也就是z
我们知道函数内部临时变量z是要被销毁的,那么如何返回z呢?
返回值
return z;
00261885 mov eax,dword ptr [ebp-8] //把z的值存入eax
通过把z的值存入寄存器eax,来返回z的值。
至此,add函数调用完毕,下面分析如何销毁。
函数栈帧的销毁
00261888 pop edi //弹出
00261889 pop esi //弹出
0026188A pop ebx //弹出
0026188B add esp,0CCh esp+occh
00261891 cmp ebp,esp
00261893 call 00261244
00261898 mov esp,ebp //把ebp的值给esp
0026189A pop ebp //弹出ebp
0026189B ret
00261888 pop edi //弹出
00261889 pop esi //弹出
0026188A pop ebx //弹出
0026188B add esp,0CCh esp+occh
00261898 mov esp,ebp
0026189A pop ebp //把ebp弹出并将值放入ebp指针
0026189B ret //返回到保存的地址。也就是main函数
此时ebp指向main函数栈底,esp指向栈顶,接着继续执行main函数。
002618F2 call 00261023
002618F7 add esp,8
002618FA mov dword ptr [ebp-20h],eax
将寄存器eax中的值放入c,也就成功返回了值。
至此,函数栈帧销毁的完整过程已经分析完了。