寄存器
EAX、EBX、ECX、EDX、ESI、EDI、ESP、EBP 寄存器详解-CSDN博客
一般寄存器:AX、BX、CX、DX
AX:累积暂存器,BX:基底暂存器,CX:计数暂存器,DX:资料暂存器
eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。
eax:通用寄存器, 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
,保存临时数据常用于返回值
ebx:通用寄存器,是"基地址"(base)寄存器, 在内存寻址时存放基地址。
ecx:是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
edx: 则总是被用来放整数除法产生的余数
eip:扩展指令指针,指向下一条要执行的指令的地址。
ebp:函数栈帧的栈底指针
esp:函数栈帧的栈顶指针
相关的汇编指令
mov | 数据转移指令 |
push | 数据入栈,同时esp栈顶指针也会随着变化 |
pop | 数据弹出指定位置,同时esp栈顶寄存器也要出栈,用一个寄存器esp接收数据 |
sub | 减法 |
add | 加法 |
call | 函数调用,1、压入返回地址2、转入目标函数 |
jump | 通过修改eip转入目标函数,进行调用 |
ret | 恢复返回地址压入eip |
函数栈帧
ebp,esp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的,
每一个函数调用,都是在为每一个函数调用开辟空间,这就是函数栈帧
函数的调用
int Add(int a,int b)
{
int z = 0;
z = a + b;
return z;
}
int main()
{
int a = 10;
int b = 4;
int ret = 0;
ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
使用vs2022编译器上调试,进入调试,调用堆栈,进入Add函数
发现在main函数之前,是由invoke_main函数调用main函数的,在invoke_main函数调用之前的就不讲了,也是一层层的调用。所以无论main,invoke_main,还是Add都是有自己的函数栈帧都会有寄存器esp和ebp来维护栈帧空间,接下来我们从main函数的栈帧创建开始讲解:
1、修改一下属性便于观察(可以排除编译器附带的代码):
改成(否)
2、转到反汇编
int main()
{
//main函数栈帧的创建
008A1840 55 push ebp
008A1841 8B EC mov ebp,esp
008A1843 81 EC E4 00 00 00 sub esp,0E4h
008A1849 53 push ebx
008A184A 56 push esi
008A184B 57 push edi
008A184C 8D 7D DC lea edi,[ebp-24h]
008A184F B9 09 00 00 00 mov ecx,9
008A1854 B8 CC CC CC CC mov eax,0CCCCCCCCh
008A1859 F3 AB rep stos dword ptr es:[edi]
//main函数的核心代码
int a = 10;
008A185B C7 45 F8 0A 00 00 00 mov dword ptr [ebp-8],0Ah
int b = 4;
008A1862 C7 45 EC 04 00 00 00 mov dword ptr [ebp-14h],4
int ret = 0;
008A1869 C7 45 E0 00 00 00 00 mov dword ptr [ebp-20h],0
ret = Add(a, b);
008A1870 8B 45 EC mov eax,dword ptr [ebp-14h]
008A1873 50 push eax
008A1874 8B 4D F8 mov ecx,dword ptr [ebp-8]
008A1877 51 push ecx
008A1878 E8 3C F8 FF FF call 008A10B9
008A187D 83 C4 08 add esp,8
008A1880 89 45 E0 mov dword ptr [ebp-20h],eax
printf("%d\n", ret);
008A1883 8B 45 E0 mov eax,dword ptr [ebp-20h]
008A1886 50 push eax
008A1887 68 30 7B 8A 00 push 8A7B30h
008A188C E8 46 F8 FF FF call 008A10D7
008A1891 83 C4 08 add esp,8
return 0;
008A1894 33 C0 xor eax,eax
}
3、函数栈帧的创建
008A1840 55 push ebp //将寄存器地址压入栈中注意这里ebp是invoke_main的栈底地址,然后esp-4(一个指针4个字节)
008A1841 8B EC mov ebp,esp //将esp的地址存放到ebp中,ebp向上移动成为main函数的栈底指针
008A1843 81 EC E4 00 00 00 sub esp,0E4h//这里esp减去16进制0xE4个大小,esp向上移动,成为main函数栈帧的栈顶指针,和ebp维护这段0XE4空间大小的空间,这段空间将存储局部变量,临时数据等
008A1849 53 push ebx //将寄存器ebx的值压入栈中,esp-4
008A184A 56 push esi //将寄存器esi的值压入栈中,esp-4
008A184B 57 push edi //将寄存器edi的值压入栈中,esp-4
//函数调用的时,首先要保存现场,函数调用后,恢复现场,否则子函数的调用将影响到主函数的运行。那么再拿这三个寄存器
//具体说ebx是基址寄存器,也是就访问内存基地址,esi是源操作数地址寄存器,edi是目的地址寄存器
//如果在子函数中被改写,函数结束后并没有改回来,那么主函数将会执行混乱,所以这三个寄存器要push进栈中,并在函数结束后pop回来
//初始化main函数的栈帧空间
//将ebp-24h的地址,放入edi中
//把9放入ecx中
//把0xCCCCCCCC放入eax中
//将从edi到ebp这段空间都初始化成0Xcc
008A184C 8D 7D DC lea edi,[ebp-24h]
008A184F B9 09 00 00 00 mov ecx,9
008A1854 B8 CC CC CC CC mov eax,0CCCCCCCCh
008A1859 F3 AB rep stos dword ptr es:[edi]
因为在main函数栈帧空间早就初始化成0XCCCCCCCC所以未初始化的变量打印才会“烫烫烫烫”
4、分析main函数中的核心部分
int a =10;
00BB185B C7 45 F8 0A 00 00 00 mov dword ptr [ebp-8],0Ah //将10存储进地址为ebp-8,这个地址就是a的地址
int b = 4;
00BB1862 C7 45 EC 04 00 00 00 mov dword ptr [ebp-14h],4 //与上面同理,word为2个字节double word==4个字节
int ret = 0;
00BB1869 C7 45 E0 00 00 00 00 mov dword ptr [ebp-20h],0 //同理
ret = Add(a, b);
00BB1870 8B 45 EC mov eax,dword ptr [ebp-14h]//寄存器eax保存[ebp-14h]地址的值4给eax
00BB1873 50 push eax //压入栈中 esp-4
00BB1874 8B 4D F8 mov ecx,dword ptr [ebp-8]//累加寄存器ecx保存[ebp-8]的值10给eax
00BB1877 51 push ecx //压入栈中 esp-4
//jump函数这里是函数的地址
00BB1878 E8 3C F8 FF FF call 00BB10B9
00BB187D 83 C4 08 add esp,8
00BB1880 89 45 E0 mov dword ptr [ebp-20h],eax
5、函数调用
00BB1878 E8 3C F8 FF FF call 00BB10B9
00BB187D 83 C4 08 add esp,8
00BB1880 89 45 E0 mov dword ptr [ebp-20h],eax
call指令是要执行函数调用逻辑,在执行call指令之前先会把call指令的下一条指令的地址先压栈,这个操作是为了解决当函数调用结束后回到call指令的下一条指令的地方继续往后执行
这上面的10和4是Add函数的形参x,y,这里的分析很好的说明了函数的传参过程以及值传递调用时,形参是实参的临时拷贝,形参的修改不会影响到实参
进入函数
int Add(int a,int b)
{
00BB1780 55 push ebp //将ebp的值压入栈中,就是地址,esp-4
00BB1781 8B EC mov ebp,esp //esp的值给ebp,ebp移动到esp
00BB1783 81 EC CC 00 00 00 sub esp,0CCh //将esp-0cc,esp向上移动
00BB1789 53 push ebx //将ebx压入栈中
00BB178A 56 push esi //将ebx压入栈中
00BB178B 57 push edi //将ebx压入栈中
int z = 0;
00BB178C C7 45 F8 00 00 00 00 mov dword ptr [ebp-8],0 //将0存入[ebp-8]中
z = a + b;
00BB1793 8B 45 08 mov eax,dword ptr [ebp+8] //将[ebp+8]的值存储到eax
00BB1796 03 45 0C add eax,dword ptr [ebp+0Ch] //将[ebp+0Ch]的值与eax相加
00BB1799 89 45 F8 mov dword ptr [ebp-8],eax //将相加的值存入[ebp-8]
return z;
00BB179C 8B 45 F8 mov eax,dword ptr [ebp-8] //把z[ebp-8]中的值放入一个临时的寄存器eax中,将计算结果作为返回值
}
执行到Add函数时也是由esp和ebp开辟的函数栈帧和main函数大同小异
1、将main函数的esp的地址保存在栈中(日后执行pop esp将保存好的esp的地址给回esp)
2、移动esp和ebp
3、将ebx、esi、edi的值保存
4、计算求和
5、将返回值放入eax放入寄存器中返回
函数销毁
00BB179F 5F pop edi //在栈顶弹出一个值,存放到edi中,esp+4
00BB17A0 5E pop esi //在栈顶弹出一个值,存放到esi中,esp+4
00BB17A1 5B pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4
00BB17A2 8B E5 mov esp,ebp //esp=ebp
00BB17A4 5D pop ebp //这里就是保存了main的栈底的地址(ebp)弹出并赋值给ebp,esp+4
00BB17A5 C3 ret //执行ret命令弹出栈顶的值,就是call指令下一条指令的地址,esp+4然后继续执行
就会回到call指令的下一条指令的地方继续执行
00BB187D 83 C4 08 add esp,8 //因为有俩个形参在栈顶所以+8跳过两个形参
00BB1880 89 45 E0 mov dword ptr [ebp-20h],eax //eax就是计算好返回值,[ebp-20h]就是ret,本次函数的返回值就是通过一个临时的寄存器将值带回来的
这就是Add函数的销毁,main函数的销毁也是一样的,回到invoke_main中