一个最简易的C函数:执行一个加法
int add(int x, int y) { return x + y; } void main() { __asm { // 下断点 mov eax, eax; } add(1, 2); return; }
VS2019中,创建项目。F5执行,右键到反汇编窗口。
这是main()的对应汇编代码:
此时的堆栈信息如图:
1 .F10(F11)逐步执行了PUSH 1,PUSH 2(函数执行前传入参数)后,ESP的值减少了2*4=8个字节
对应堆栈信息如下:
寄存器内容:
可以看到ESP由原来的A88变成了A80,正好减少了8个字节。
2.F11单步进入函数执行,这里需要注意的是VS执行函数前会先通过JMP指令跳到函数对应的地址,如图:
此时继续F11执行即可进入函数内查看分步信息。
3.这是add函数执行前的堆栈及寄存器信息:
4.函数的执行使用了EBP寻址的方式保证了函数执行前后的堆栈平衡:
EBP寻址以前也有记录,这里介绍add函数对应汇编指令的含义:
(1)把EBP放入堆栈,同时让栈顶栈底指向同一处
PUSH EBP MOV EBP,ESP
此时ESP与EBP的状态如图:
(2)开辟当前函数用到的内存,开辟了C0个这么大的内存:
SUB ESP,0C0h
C0/4=30,即开辟了30个4字节的内存(30格——3*16=48格格<10进制>),此时的堆栈信息如图:
一格为4字节,sub esp,c0就是开辟了c0个字节的内存,即48格。
且此时的ESP减少了C0,变成了009EF9B8,而EBP保存了原来的ESP的值。
(3) 因为函数中可能用到寄存器,但是这个寄存器的内容,在当前函数执行结束以后,后面的程序可能用到,所以需要先将用到的寄存器的内容先push入栈保存起来:
PUSH EBX PUSH ESI PUSH EDI
对应堆栈信息:
(4)向缓冲区(函数开辟的那块内存)循环填入CC程序断点,防止缓冲区溢出.
LEA EDI,[EBP-0C0h] MOV ECX,030h MOV EAX,0CCCCCCCCh REP SOTS DWORD PTR ES:[EDI]
MOV ECX,030h
MOV EAX,0CCCCCCCCh
REP SOTS DWORD PTR ES:[EDI]
这三行指令的作用是循环执行把EAX的值放到EDI的位置,循环次数为ECX内保存的值(30h<48>次),正好是当前函数开辟的内存大小。
执行结束后堆栈信息如下:
且这几行指令执行结束后ESP与EBP的值并没有改变。只是把缓冲区的内容全部填充成了CCCCCCCC断点。以便在缓冲区溢出时及时停止
(5) 函数执行1+2:把传入的参数x,y相加并保存到eax中
mov eax,dword ptr [x] add eax,dword ptr [y]
此时EAX:
(6)接下来就是恢复堆栈:
①还原EDI,ESI,EBX
POP EDI POP ESI POP EBX
②还原ESP
ADD ESP,0C0h
此时堆栈应该ESP=EBP=009EFA78
③还原EBP:把堆栈中存在栈顶的EBP的值POP到EBP中
POP EBP
堆栈信息如图:
④return:表示add函数执行结束,这里的堆栈已经回到了进入add()函数之前的状态。
RET
堆栈如图:
(6)因为还有PUSH两个参数造成的两段已经使用过的内存A84和A88,所以在main()函数中还要进一步恢复:
ADD ESP,8
此时的堆栈如图:
这样:ESP和EBP就回到了这个add函数执行之前的样子