我们用一个简单的求和函数来探索函数的调用过程
#include<stdio.h>
#include <stdlib.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int sum = Add(a, b);
printf("sum=%d\n", sum);
system("pause");
return 0;
}
按F10进入调试,并转到反汇编。
首先是main函数调用的过程。
1.ebp和esp是用来维护函数的栈底指针和栈顶指针,push ebp,即压栈,将__mainCRTStarup函数的ebp压栈,在它的栈顶开辟一块空间放入它的ebp;
2.将esp给ebp,此时ebp与esp同时指向刚刚开辟空间的顶端;(创建栈帧的过程)
3.esp减去4Ch大小的值,我们知道栈空间中元素存放顺序是由高地址到低地址,则该步骤在ebp的上面开辟了4Ch大小的内存空间;
4.ebx,esi,edi三块空间进行压栈,随着压栈的进行,esp指向edi的顶端;
5.将ebp-4Ch这个地址放到edi中(方便之后ret步骤的使用);
6.lea 加载有效地址初始化与开辟空间为0Xcccccccc。
7.创建变量a,b,ret。
8.“ebp-0Ch”放入了0,执行int ret =0这一步骤。
9.“ebp-8”存放着b的值,将它mov给eax寄存器并且压栈;同理“ebp-4”存放着a的值,将它mov给ecx寄存器并且压栈。
10.接着调用call指令。注意,在调用call指令的时候,在ecx的上方又开辟了一块空间用于存放call指令下一条指令的地址(这个地址的作用是在call指令调用add函数结束的的时候jump指令能够找到call指令下一条指令的地址,从而回到main函数中),很关键。
11.由call指令进入add函数之后,第一步就是进行压栈,将main函数的ebp压栈保存在上面开辟的空间中。
12.接下来函数开辟空间和初始化跟main函数是一样的。
13.创建z空间,地址为abp-4,放进去内容为0;
14.“ebp+8”指向x=10的存储空间,将10放到寄存器eax中;同理“ebp+0Ch”指向y=10的存储空间,将20add到eax中,此时eax中的内容为30;
15.将eax给“ebp-4”,即z这块空间,意思为将z的内容由零改为30;
16.将z的值放回到eax中,这一步骤代表着返回机制,z的值将由寄存器eax带回到main函数中。
17.之后执行pop出栈操作,edi,esi,ebx三块空间出栈后,esp向下移动,这三块空间已经不属于add函数,但是依然存在。
18.将ebp给esp,此时esp回退到当前ebp的位置。
19.之后对下一块空间(即保存着main函数ebp的空间由)执行出栈操作, 由于保存的是main函数的ebp,栈底指针ebp得以回到main函数ebp的位置。
20.执行ret指令,相当于继续pop下一块空间,我们知道存放main函数ebp空间的下一块空间,存放的是call指令下一条指令的地址,那么在ret执行完成之后,程序返回到main函数中call指令的下一条指令。
21.call指令的下一条指令就是“add esp,8”,即将esp向下移动8个字节,将刚刚调用的两个实参10,20也弹出去。
22.把eax的值放到“ebp-0Ch”(即c的空间)中,一致eax中存着30,至此c的值已被赋值为30。
23.eax寄存器已经没有用处,需要归零,我们执行xor(异或)这一步骤,两个相同的eax异或归零。
24.接下来就是栈的销毁。