目录
一、寄存器
eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。
这些32位寄存器有多种用途,但每一个都有各自的特别之处。
eax:累加寄存器,相对于其他寄存器,在运算方面比较常用。
ebx:基地址寄存器,作为内存偏移指针使用。
ecx:计数器,用于特定的技术。
edx:作为EAX的溢出寄存器,(除法产生的余数)。
eip:存储CPU下次所执行的指令地址(存放指令偏移地址)。
esp:指针的寄存器,用于堆栈操作。被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp也就越来越小。在32位平台上,esp每次减少4字节。
ebp:基址指针,指栈的栈底指针。
二、内存
概论栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
演示环境:window 10 && vs2013
原因:编译器越高级,其中处理的越会繁琐,越高级的编译器,越不容易学习和观察。
同时在不同的编译器下,函数的调用过程中的栈帧的创建是略有差异的,具体细节取决于编译器的实现;
三、演示函数栈帧的创建销毁过程
先来了解两个寄存器 分别为:ebp,esp
这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。
//下面为本次演示的代码块
#include<stdio.h>
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", c);
system("pause");
return 0;
}
首先当代码被编译的时候 main函数会在内存中申请到一块空间
然后我们按下F10,并且打开调试的堆栈窗口可以发现,main函数被调用起来了
那么我们新的问题来了 main函数被谁调用了?
莫慌 我们快进到代码运行结束
我们可以看到, main 函数调用之前,是由 invoke_main 函数来调用main函数。那我们可以确定, invoke_main 函数应该会有自己的栈帧, main 函数和 Add 函数也会维护自己的栈 帧,每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间。
那接下来我们从main函数的栈帧创建开始说起:
那么栈帧的创建又是如何实现的呢?
我们来转到反汇编来look一下
int main() { 004A1410 push ebp 004A1411 mov ebp,esp 004A1413 sub esp,0E4h 004A1419 push ebx 004A141A push esi 004A141B push edi 004A141C lea edi,[ebp-0E4h] 004A1422 mov ecx,39h 004A1427 mov eax,0CCCCCCCCh 004A142C rep stos dword ptr es:[edi] int a = 10; 004A142E mov dword ptr [a],0Ah int b = 20; 004A1435 mov dword ptr [b],14h int c = 0; 004A143C mov dword ptr [c],0 c = Add(a, b); 004A1443 mov eax,dword ptr [b] 004A1446 push eax 004A1447 mov ecx,dword ptr [a] 004A144A push ecx 004A144B call _Add (04A10E1h) 004A1450 add esp,8 004A1453 mov dword ptr [c],eax printf("%d", c); 004A1456 mov esi,esp 004A1458 mov eax,dword ptr [c] 004A145B push eax 004A145C push 4A5858h 004A1461 call dword ptr ds:[4A9114h] 004A1467 add esp,8 004A146A cmp esi,esp 004A146C call __RTC_CheckEsp (04A113Bh) system("pause"); 004A1471 push 4A58B0h 004A1476 call _system (04A11E0h) 004A147B add esp,4 return 0; 004A147E xor eax,eax } 004A1480 pop edi 004A1481 pop esi 004A1482 pop ebx 004A1483 add esp,0E4h 004A1489 cmp ebp,esp 004A148B call __RTC_CheckEsp (04A113Bh) 004A1490 mov esp,ebp 004A1492 pop ebp 004A1493 ret
可以看到是执行了非常多道“工序” 那么我将它为分三个模块来说
一、main函数调用
push ebp //把ebp寄存器中的值进行压栈,此时的ebp中存放的是invoke_main函数栈帧的ebp,esp-4
mov ebp,esp //move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp
sub esp,0E4h //sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据已经调试信息等。
push ebx //将寄存器ebx的值压栈,esp-4
push esi //将寄存器esi的值压栈,esp-4
push edi //将寄存器edi的值压栈,esp-4
lea edi,[ebp-0E4h] //先把ebp-0E4h的地址,放在edi中
mov ecx,39h//把39放在ecx中
rep stos dword ptr es:[edi]//将从edp-0x2h到ebp这一段的内存的每个字节都初始化为0xCC
二、main函数中的核心代码
dword ptr [a],0Ah //将10存储到0Ah的地址处,0Ah的位置其实是a变量
mov dword ptr [b],14h //将20存储到14h的地址处,14h的位置其实是b变量
mov dword ptr [c],0 //将0存储到0的地址处,0的位置其实是c变量
三、函数传参
c = Add(a, b);
//调用Add函数时的传参
//其实传参就是把参数push到栈帧空间中。
moveax,dword ptr [b]//传递b,将ebp-14h处放的20放在eax寄存器中
push eax //将eax的值压栈,esp-4
mov ecx,dword ptr [a]//传递a,将0Ah处放的10放在ecx寄存器中
push ecx //将ecx的值压栈,esp-4
call _Add (04A10E1h)//跳转调用函数,将下一条指令的地址压入栈中
add esp,8
mov dword ptr [c],eax
call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈 操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。
int Add(int x, int y) { 00BE1760 push ebp //将main函数栈帧的ebp保存,esp-4 00BE1761 mov ebp,esp //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp 00BE1763 sub esp,0CCh //给esp-0xCC,求出Add函数的esp 00BE1769 push ebx //将ebx的值压栈,esp-4 00BE176A push esi //将esi的值压栈,esp-4 00BE176B push edi //将edi的值压栈,esp-4 int z = 0; 00BE176C mov dword ptr [ebp-8],0 //将0放在ebp-8的地址处,其实就是创建z z = x + y; //接下来计算的是x+y,结果保存到z中 00BE1773 mov eax,dword ptr [ebp+8] //将ebp+8地址处的数字存储到eax中 00BE1776 add eax,dword ptr [ebp+0Ch] //将ebp+12地址处的数字加到eax寄存中 00BE1779 mov dword ptr [ebp-8],eax //将eax的结果保存到ebp-8的地址处,其实就是放到z中 return z; 00BE177C mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。 } 00BE177F pop edi 00BE1780 pop esi 00BE1781 pop ebx 00BE1782 mov esp,ebp 00BE1784 pop ebp 00BE1785 ret
在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。 将main函数的 ebp 压栈 计算新的 ebp 和 esp 将 ebx , esi , edi 寄存器的值保存 计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的 参数,这就是形参访问。 将求出的和放在 eax 寄存器准备带回
00BE177F pop edi //在栈顶弹出一个值,存放到edi中,esp+4 00BE1780 pop esi //在栈顶弹出一个值,存放到esi中,esp+4 00BE1781 pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4 00BE1782 mov esp,ebp //再将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈 帧空间 00BE1784 pop ebp //弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈底。 00BE1785 ret //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行
调用完Add函数被释放后 寄存器中存储的就是Add函数的返回值 这也可以解释为何调用函数只能传回一个值
四、总结
希望可以对大家有所帮助,如有错误请大家指出 万分感谢!