前言
在这一次的文章中,我们会解决以下几个问题
- 局部变量是怎么创建的?
- 为什么局部变量不初始化时的值是随机的?
- 函数是怎么传参的?传参的顺序是怎么样的?
- 形参和实参是什么关系?
- 函数调用结束后是怎么返回的?
预备工作
欲解决以上问题,施主还得沉下心来慢慢领会
首先介绍一下
寄存器
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,一个来自重庆的女孩,现在在读大二,希望和大家一起学习,一起进步!!!