本文将在VS2012环境下对函数调用过程的汇编代码进行分析。分析不到位或者存在错误的地方请批评指正,请与作者联系。
#include
#include
#include
#include
#include
#include
#include
using namespace std; int test(int a,int b){ int c; c = a + b; return c; } int main(){ int a =3,b =5; test(a,b); system("pause"); }
我们在main函数第一行代码处开始单步调试。
ebp寄存器通常指向栈底,常用来寻址当前函数内的局部变量等。esp是指向堆栈栈顶的指针。X86/64 vc++的堆栈是从高地址向低地址生长,比如push一个32位寄存器入栈后,esp - 04H。
第一行是push ebp,意思是将ebp入栈保护起来,虽然这个函数是main函数,但是流程和一般函数调用一样,都是先将ebp入栈保护。
mov ebp,esp 是将当前栈顶指针给ebp,使其能够寻址局部变量。
sub esp,0D8H 把栈顶指针esp移动到0D8H之后,这样留出一块隔离的空间,这个隔离空间的大小是由编译器决定的。默认的隔离空间是192字节。如果定义一个int会在栈空间里面占用12字节?局部变量也是存在隔离取内部的。< http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+cHVzaCBlc2k8L3A+CjxwPnB1c2ggZWR1PC9wPgo8cD5wdXNoIGVieDwvcD4KPHA+ysexo7ukz9azobXEM8z1u+Ox4Na4we6hozwvcD4KPHA+0qrA7b3ibGVhIGVkaSxbZWJwLTBEOEhdILXE0uLLvKOsz8jSqrjjx+Wz/mxlYbXE0uLLvKGjPC9wPgo8cD682cnoo7pTST0xMDAwSCAsIERTPTUwMDBILCAoNTEwMDBIKT0xMjM0SDwvcD4KCqGhoaHWtNDQ1rjB7iBMRUEgQlggLCBbU0lduvOjrEJYPTEwMDBICgqhoaGh1rTQ0Na4we4gTU9WIEJYICwgW1NJXbrzo6xCWD0xMjM0SDxicj4KPHA+PC9wPgo8cD7KtbzKyc++zcrHyKFbXcTase3KvrXEyv2+3bXExqvSxrXY1rehozwvcD4KPHA+PC9wPgo8cD4gbGVhICAgICAgICAgZWRpLFtlYnAtMTA4aF0gIDwvcD4KPHA+IG1vdiAgICAgICAgIGVjeCw0MmggIDwvcD4KPHA+IG1vdiAgICAgICAgIGVheCwwQ0NDQ0NDQ0NoICA8L3A+CjxwPjwvcD4KPHA+cmVwIHN0b3MgICAgZHdvcmQgcHRyIGVzOltlZGldPC9wPgo8cD48YnI+CjwvcD4KPHA+1eLLxL7kveG6z9Ta0rvG8L7Nyse21MH0s/a1xLj0wOvH+NPyus2+1rK/seTBv8v51by1xL/VvOS9+NDQx+W/1aOs1sPOqjB4Q0NDQ0NDQ0NIoaO9q8O/uPbX1r3a1sPOqjB4Q0NIysfT0NSt0vK1xKOsMHhDQ0jKx2ludCAztcTX1r3awuujrGludCAzyrG2z7Xj1tC2z9a4we6ho7Wx0uLN4rXE1rTQ0MHL1bvW0LXExNrI3cqxo6y74daxvdPM4cq+tO3O86GjPC9wPgo8cD7KtbzKyc/Kx1NUT1PWuMHutcTX99PDyse9q2VheNbQtcQmIzIwNTQwO7+9sbS1vUVTOkVESda4z/K1xLXY1reho3JlcNa4we7Kx9bYuLTWuMHuo6zW2Li0tM7K/bTm1Nq8xLTmxvdlY3jW0KGjNDJIyscxML341sa1xDY2o6y+zcrH1ti4tDY2tM6ho8O/tM69q9K7uPbX1rXEv9W85NbDzqplYXi1xCYjMjA1NDA7o6zW2Li0Nja0zr7Nx+W/1cHLMjY019a92qGj1f26w72ruPTA68f4us2+1rK/seTBv8v51by/1bzkx+W/1aGjPC9wPgo8YnI+CjxpbWcgc3JjPQ=="https://www.cppentry.com/upload_files/article/55/1_t6ddn__.png" alt="\">
mov dword ptr [a],3
mov dword ptr [b],5
将立即数写入栈空间。
下面是内存状况。
图1
因为下面要调用test函数,所以将a,b两个int型变量入栈。
入栈顺序为:
1.cdecl方式:C语言方式,参数从右边开始入栈。
2.winapi方式:参数还是从右边入栈。
上图是test函数的汇编代码。可以看出,与main函数的过程十分相似。首先也是将ebp入栈保存,因为这个时候的ebp还是main函数的,所以保存起来,然后把栈顶指针esp给ebp。现场保护和清空栈区域的代码与main函数中的几乎相同。
下一句是获取参数信息的。
mov eax,dword ptr [ebp+8]
这句ebp+8,实际上就是当前栈顶往栈底移动8个字节。从这里开始往后的4个字节放入eax。下面那句汇编意思相同。
ebp+8的原因是,在调用call命令的时候会将当前的EIP指针入栈。所以,这里后移8字节就能访问到参数了。
mov dword ptr [ebp-8],eax 这句将计算结果放入ebp-8的位置,也就是局部变量c所在的栈空间。
mov eax,dword ptr [ebp-8] 这句将[ebp - 8]栈的内容放入eax。函数的返回值就是通过eax寄存器返回的。
下面是内存状况:
图2
下面几句恢复现场。
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
mov esp,ebp 这句将当前基指传给栈顶指针。因为在test函数调用时,堆栈已经生长到这个地方了。
RET指令的内部操作是:栈顶字单元出栈,其值赋给IP寄存器。此时,IP指针就回到main函数继续执行了。
最后返回main函数后会执行一句
add esp,8 这句的意思是平衡刚才有传参占用的堆栈空间。从图2中可知,ret后,esp会回到参数b的下面。此时,+8会使esp回到调用test前的位置,即恢复了栈顶指针。
简单函数调用的汇编分析到此结束。下次分析new、malloc的区别。以及不同段的区别。