相关概念
eax,ebx,ecx,edx都是寄存器的名称.
ebp和esp也是寄存器,这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的.
每一个函数调用都要在栈区上创建一个空间. 为这个函数开辟的空间就叫做这个函数的函数栈帧.
假设现在有这样一段代码:
ebp叫做栈底指针,esp叫做栈顶指针.栈的使用习惯是先使用高地址在使用低地址.
在vs2013中main函数也是被其他函数调用的.不同的编译器,函数调用过程中栈帧的创建略有差异,具体细节取决于编译器的实现.
main函数被__tmainCRTStartup函数调用,__tmainCRTStartup被mainCRTStartup函数调用.
将代码转到反汇编来查看具体的细节.
由于此时ebp和esp指向的是调用main函数的函数的栈顶和栈底,此时调用main函数之后,就要为main函数开辟一块空间作为main函数的函数栈帧.
此时先将ebp压栈,这个ebp的值记录的是__tmainCRTStartup函数的栈底.
压栈之后,esp会自动指向栈顶元素.
mov ebp,esp,将esp的值付给ebp,此时ebp就指向了栈顶,为新开辟一块空间做准备.
sub esp,0E4h,将esp的值减去0E4h,那么0E4h就是为main函数预开辟的栈帧大小,这块空间会给main函数来使用.
push ebx,push esi,push edi压进去三个值,在后续操作中会使用到.
lea的意思是load effective address,加载有效地址,把后面的地址加载到edi中..
word是双字,dword就是四字.
这段操作的意思就是从末尾地址开始,每次写四个字节,一共写39h次,写的内容为0CCCCCCCCh.
就是为刚才创建好的main函数的函数栈帧进行初始化工作,所以c语言的变量一定要初始化,要不然就是随机值,我们也不知道会初始化为什么,这是由编译器决定的.
.
走到这里,就完成了main函数栈帧的开辟和初始化.
将0Ah也就是a的值放到ebp-8的位置.(指定了变量a的存储空间并把a的值放进去)
将14h也就是b的值放到ebp-14的位置.
将0也就是c的值放到ebp-20的位置.
所以如果变量不赋值,变量的空间里存储的就是0CCCCCCCCh.
局部变量的创建在栈上也不一定是连续的,可能会跨过一些空间,取决于编译器的实现.
接下来就开始调用Add函数.
将ebp-14h里的值放到eax中,再将eax压栈.也就是将b的值压栈.
把ebp-8里的值放到ecx中,再将ecx压栈,也就是将a的值压栈.
所以这两个操作就是在传参.
call调用函数.call在调用add函数的同时,会将call指令下一条指定的地址压栈,也就是将00C21450进行压栈.因为调用完add函数之后还要回来执行下面的指令,所以要记录一下.
执行add函数.
和main函数的逻辑一样,先为add函数开辟空间和初始化.
接下来将ebp-8的地址里放入一个0也就是z的值.
把ebp+8里的值放入eax,在把ebp+12的值加到eax,也就是10+20就是30了.
加完之后再把eax的值放入z的空间里.
所以传参的变量根本不是再add的函数栈帧里创建的,而是再真正的调用add函数之前,我们就完成了参数的传递,将参数进行压栈,等到add函数里用到形参的时候,再回头去找已经入栈的形参的值.
所以形参是实参的一份临时拷贝,在add函数里面改变形参的值根本不会影响到实参.
参数是从右向左传的,先传的b再传的a.
在执行完z=x+y之后,进行返回return z.
在返回的时候,是将z中的值放到了eax寄存器中,进行保存,因为add函数执行完之后,开辟的空间就都被销毁了,包括z.所以要将返回的内容先保存到寄存器当中.
.
函数执行完之后,接下来要进行add函数栈帧的销毁了,先执行三个pop.
在将ebp的值赋给esp,此时esp就指向了ebp所指的位置,那么上面的add的空间就没有变量指向了.
接下来进行pop,将弹出的指赋给ebp.
这次的pop弹出的是ebp指向main函数的那个地址,我们在之前是压栈进去的.
所以,此时ebp就重新指向了main函数的函数栈帧的首地址.
ret指令就是从栈顶弹出了当时记录的call指令的下一条指令的地址.
这样就顺顺利利的回到了main函数里.
当回到main函数的时候,就回到了call指令的下一条指令.
我们回到main函数里,执行add esp 8,也就是让esp指向了下面的地址,跨过了8个字节.
这八个字节就是形参的空间,也就是将形参销毁,将空间还给操作系统了.
在将eax的值放到ebp-20h中,也就是c中.此时eax的值就是函数的返回值,也就是将返回值赋给c了.
上述过程就是函数栈帧的创建和销毁,并且还有函数的调用和返回.
几个问题
局部变量是如何创建的?
函数栈帧开辟好后,为局部变量分配一些空间,供局部变量的使用和初始化.
为什么局部变量不初始化是随机值?
随机值是在进行函数栈帧初始化的时候放进去的.
函数如何传参?
在真正调用函数之前,先将形参进行压栈,当真正进入函数之后,在函数的栈帧里面,通过指针的偏移量在回头找到形参的值.
形参和实参的关系?
形参是在函数调用之前,进行压栈的时候得到的空间,它和实参在值上是相同的,但是空间上独立的.所以形参是实参的一份临时拷贝,改变形参的值并不会影响到实参.
函数调用是如何做的?
在调用函数之前,就把call的下一条执行的地址压栈了,同时将ebp指向的当前函数的首地址压栈了.
在函数执行完的时候,弹出元素,就能找到上一个函数ebp的指向了,在弹出一个元素,就能找到call指令的下一条指令的地址了.
返回值是通过寄存器的方式带回来的.