前言
在这一次的文章中,我们会解决以下几个问题
- 局部变量是怎么创建的?
- 为什么局部变量不初始化时的值是随机的?
- 函数是怎么传参的?传参的顺序是怎么样的?
- 形参和实参是什么关系?
- 函数调用结束后是怎么返回的?
预备工作
欲解决以上问题,施主还得沉下心来慢慢领会
首先介绍一下
寄存器
eax
ebx
ecx
edx
ebp
esp
这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。
由于vs2013以上的IDE封装的太好,看不到,所以这一次我们使用vs2013来调试观察
正式开始
首先我们先编写这样一段简单的代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
然后F10启动调试,右键反汇编我们可以看到
在vs2013中,main函数也是被其他函数()调用的。
现在我们来理解一下汇编代码
可以得出结论:经历了push操作之后在内存块里面压进了ebp
现在我们来看一下内存里的表现,到底有没有把ebp压进内存中
通过上图可以看到esp里存放了ebp的地址,所以上述结论是成立的
接下来我们继续下一步–mov ebp,esp
也就是把ebp移到esp的位置。
通过观察上图,可以看到确实如此
接下来我们继续下一步,sub esp,0E4h
就是将esp的值减去0E4h
esp的值减小,如下图
ebp和esp就为main函数预开辟了一块空间
现在我们继续后面的操作
可以看到函数栈帧里压进去了ebx,esi,edi三个寄存器
如下图:
我们接着后面的操作
可以理解为从edi开始将eax中的值(CCCCCCCC)每次拷贝dword(double word,一个word两个字节)重复执行ecx次.,拷贝到es:[edi]指向的地址(ebp)。简而言之,就是把edi到ebp这部分空间初始化为(CCCCCCCC)。
如下图:
我们继续接下来的操作,
mov eax,dword ptr [ebp-8],0Ah将0Ah(10)移到ebp-8的位置
同理,后面两句执行后的效果如下图:
现在来执行下面这个句子
我们可以这样理解,mov 将ebp-14h中的值(也就是a的值20)存到eax里,然后将eax 压栈进去,同时esp会往上移动维护栈
这即是在实现int a = 20;
同理可得,还会压进去ecx(里面存储了b的值10)
这即是在实现int b = 10;
执行完这四个句子之后
同时我们也可以观察到,在调用函数的时候我们是先写的a,然后是b,但是在传参的时候是先传的b,然后是a
再点击F11之后再次来到了add的位置,
执行push ebp,往栈顶压入ebp
执行mov ebp,esp
可以看到ebp和esp指向同一个位置
现在来执行sub esp,0CCh
可以看到esp又往低地址的地方移动,所以和为main
函数开辟函数栈帧一样,为add函数也开辟了函数栈帧
如下图:
现在往栈帧里面压进3个寄存器ebx,esi,edi
接下来我们继续看后面的指令
001813CC lea edi,[ebp+FFFFFF34h]
001813D2 mov ecx,33h
001813D7 mov eax,0CCCCCCCCh
001813DC rep stos dword ptr es:[edi]
与main函数里面的那一段代码的用处相同,就是把地址从edi到esp的值都用cccccccc来初始化
下面接着执行mov dword ptr [ebp-8],0
即是将0放到ebp-8的位置上,就是在实现我们写的int z = 0 这一句c语言代码
如下图:
可以通过监视和观察地址来验证一下
会发现我们确实将0放到ebp-8的位置上了
下面执行黄色框中的内容,我们可以看到此时ebp+8与ebp+12就是x与y的值,但是这两个值都是通过前面将ab的值直接拷贝到寄存器eax,ecx中得到的,我们就算改变了形参xy值所在的寄存器里面的值,a与b都不会改变。所以形参是实参的临时拷贝,形参的改变不会引起实参的变化
同时可以看到我们的形参不是在add函数内部创建的,而是回到了main函数中压栈压进去的空间去找。
首先我们待会儿要解决的问题是,临时变量出了函数作用域就会被销毁,那么是如何返回的呢?
现在让我们来观察返回的过程
可以看到是先把z中的保存到一个相当于全局变量的一个寄存器eax里面
然后我们接着执行mov esp,ebp
执行后ebp和esp指向同一个地方,就实现了对add函数开辟栈帧的回收
我们都知道程序从main函数开始,结束于main函数
那么add函数完了之后,我们怎么样回到main函数呢?
现在执行add函数里的最后一条指令 001813F7 ret
按F10之后我们发现程序来到了call指令的下一条指令的位置
好的现在程序是已经回到了main函数了
接着后面的操作 add esp,8
(就是把esp+8)
可以看到此时把形参xy分分配的空间也还给了操作系统
继续下一步操作 mov dword ptr [ebp-20h],eax
可以看到通过这条指令和前面将最后的结果存到eax的操作,我们就成功的带回了add函数的返回值
总结
现在来回答一下最开始的那几个问题
局部变量是怎么创建的?
首先为函数预开辟一块空间,然后局部变量在这一块栈帧里面分配一点空间为什么局部变量不初始化时的值是随机的?
随机值是系统为函数开辟空间时,随机放到函数开辟空间里的值(CCCCCCCC),只要初始化,就会覆盖掉原来的值函数是怎么传参的?传参的顺序是怎么样的?
当要调用函数时,已经将形式参数从右到左push到了函数栈帧里面形参和实参是什么关系?
形参是实参的临时拷贝,形参的改变不会引起实参的改函数调用结束后是怎么返回的?
在调用函数之前,就先保存call指令的下一条指令的地址和main函数中的ebp的地址,当函数调用结束时通过pop,ebp回到main函数里。然后通过ret指令我们回到了call指令的下一条指令,就回到了main函数里。
最后我是Maria,一个来自重庆的女孩,现在在读大二,希望和大家一起学习,一起进步!!!