文章目录
本章详细讲解函数调用、传参、压栈
每一次函数调用,都要在栈区创建一个空间,有两个寄存器esp、ebp来维护这块空间,正在调用哪个函数,esp、ebp就在维护哪块栈帧。
不同编译器下,能够观察到的函数调用过程不一样。
我们在VS2013编译器下,main函数是被谁调用的?
mian函数被__tmainCRTStartup函数调用;__tmainCTRTStartup函数被mainCRTStartup函数调用。
现有如下代码:
int Add(int n,int m)
{
int c = n + m;
return c;
}
int main()
{
int n = 10;
int m = 20;
int c = Add(n,m);
printf("%d\n",c);
return 0;
}
我们通过反汇编查看代码执行过程:
1.为__tmainCRTStartup函数开辟栈帧:
此时寄存器esp和ebp值如下图:
然后ebp压栈
ebp压栈(在栈顶上面放元素,并不是栈顶和栈底之间放),栈顶指针要移动,栈顶指针esp地址减少4个字节空间的地址,因为32位平台,指针大小是4个字节。
将esp的值赋值给ebp
2.为main函数开辟栈帧
0E4h是10进制228,将esp的值减去0E4h,esp的值变小,向低地址处移动,那么此时,esp(地址为0063fb40)~ebp(地址为0063fc24)之间的228个字节空间都是为main函数开辟的。
ebx入栈:从栈顶位置开始存放
esi入栈:
edi入栈:
将ebp-24h存入edi中
下图中三行代码的意思是:从edi开始,向下9*4个字节的空间,内容全部赋值为eax中的值。
一个word是2个字节,dword是4个字节,将edi后面36个字节的值全部初始化为0xCCCCCCCC,然后edi=ebp;至此,已经为main函数开辟好栈帧。然后执行call指令,开始调用main函数
3.在main函数中创建局部变量n,m
给n分配空间,并初始化,值为10;单步执行,如下,所以如果创建局部变量不给初始化,默认值就是随机值,VS下随机放入0xCCCCCCCC,不同编译器下放的随机值不同。
继续执行,将14h放到ebp-14h中
因为创建局部变量c没有初始化,所以从c默认值为随机值。
下面开始准备调用Add函数:
1.函数传参:
将参数m的值传给eax,然后eax压栈
继续执行,把n的值赋值给ecx,然后ecx压栈
2.调用Add函数,为Add函数准备栈帧
执行call指令,调用Add函数
其中006819f0是call指令的下一条指令的地址,因为调用Add函数结束,还要返回到调用的地方,继续执行下一条指令
继续单步执行,进入Add函数内部
为Add函数准备栈帧
接下来将ebx、esi、edi压栈
接下来,edi = ebp - 0Ch = 0x0063fb18
ecx的值为3
eax的值为0xCCCCCCCC,然后为栈帧空间初始化一些值,
从edi地址开始向下3*4个字节的空间全部赋值为0xCCCCCCCC
因为变量c还没给初始值时,默认值为随机值
找到传过来的参数,将n的值给eax,n在ebp+8的空间上,然后将eax加上m的值,m的值在ebp+12的空间上,再把eax的值给c
将c的值放到eax中,然后edi、esi、ebx依次出栈,esp栈顶指针增大
将esp加上CCh(204个字节)
然后比较esp和ebp的值,接着ebp出栈(main函数ebp出栈),这里pop ebp的意思是把栈顶的元素弹出来,放到ebp中去,这里的ebp是之前存储的main函数的ebp,所以弹出ebp后,ebp又回到main函数的ebp了,那么esp随着栈顶元素的出栈,回到main函数,然后esp和ebp维护main函数栈帧空间,ebp是寄存器,我们存储在空间的都是地址。寄存器是独立于内存的,是集成到cpu上面的,在代码中(内存中)都是可以使用寄存器的。
执行ret指令
回到Add函数call指令下一条指令的地方
然后将esp+8
将寄存器eax的值给c。
1.局部变量是怎么创建的?
先给函数开辟栈帧,然后给栈帧空间初始化一些值(例如0xcccccccc),然后在栈帧中给局部变量分配空间。
2.为什么局部变量的值是随机值?
因为我们会先放入一些随机值,然后创建局部变量时,如果给初始值,那么初始值就会覆盖随机值,如果没有初始化,那么默认值就是随机值。
3.函数是怎么传参的?
在调用函数之前,先进行传参,将形参从右向左压栈。在函数内部是通过指针偏移量来找到形参的(在栈帧内部并不为形参再分配空间)。
4.形参和实参是什么关系?
形参是实参的一份临时拷贝,改变形参,不会影响实参的变化。
5.函数调用是怎么实现的?
先进行传参,然后执行call指令,记住call指令后面的一条指令的地址,然后为函数开辟栈帧,为栈帧空间初始化一些值,在栈帧中为局部变量分配空间(初始化,不初始化默认为随机值),在函数内部使用形参,根据指针偏移量来获取形参的值,在栈帧内部是不为形参分配空间的。
6.函数调用结束后怎么返回的?
在调用函数之前,先把call指令的下一条指令的地址压栈,然后将上一个函数的栈底指针ebp先压栈存储起来,当前函数调用完毕,弹出ebp,找到上一个函数的ebp,回到上一个函数的栈帧空间,然后回到上一个函数中调用该函数的地方。返回值通过寄存器的方式带回上一个函数,寄存器不会随着函数调用结束就销毁。