函数调用需要先为函数开辟一个栈帧空间,就像在创建变量时,会向操作系统申请相应大小的n个字节的空间
cpu中的两个寄存器ebp,esp分别指向函数栈帧的栈底位置和栈顶位置,两个寄存器目前的指向取决于程序目前正在执行哪个函数,就拿下面的程序分析:
目前的程序运行到11行,执行在main函数的栈帧内,打开内存,找一下ebp和esp的位置:
ebp指向main函数的栈底位置,esp指向main函数的栈顶位置:
从F6A0到F680,一共是32个字节,mian()函数栈帧的总大小就是32个字节.
首先我们需要清楚,main()函数其实也是被别的函数调用,调试开始后我们观察下栈帧:
如图:main()函数被_tmainCRTSrartup()函数调用,_tmainCRTSrartup()函数又是被mainCRTStarup()函数调用,抽象出来内存如图所示:
目前我们只需要知道main()函数也是被其他函数调用的就可以,至于调用它的函数实现的什么功能,我们不需要知道.
要观察函数栈帧具体是怎样创建和销毁的和了解ebp和esp两个寄存器是如何对函数栈帧进行维护的,我们需要转到汇编代码:
让我们解释下汇编代码,当程序运行到这里的时候,从花括号到第一个变量a之间的汇编代码就是c语言花括号的含义,他被解释成汇编代码实际上就是在为main()函数的栈帧创建进行准备工作:
我们还是先把函数栈帧入抽象一下:
push ebp:
他表示将ebp寄存器压栈,ebp的数值就是当前函数栈帧的栈底地址,元素入栈后,栈顶指针向上移动:
观察esp的值,向上走一步,上面是低地址,esp的值会减少:
可以看到刚好减少了四个字节:同样可以观察到esp内的值:
move ebp,esp : 将ebp移动到esp的位置
sub esp 0E4h:esp指针减去0E4h(228)
图中,上面是低地址,下面是高地址,esp减去一个值,表示esp向上走了,减去的这个值就是为main()函数开辟的栈帧的空间大小.
push ebx ,push esi,push edi: 将ebx,esi,edi三个寄存器的值压栈:
lea edi [ ebp + FFFFFF1ch] : 方括号内的内容还可以表示成 ebp-0E4h
前面的一条mov指令,将ebp移动到esp的位置后,又给esp减去了0E4h
lea的全拼是 load effective address ,加载有效地址,即edi中存放的地址实际是main()函数的栈顶位置.
mov ecx 39h,mov eax 0xcccccccch: 将39h和0xcccccccch分别加载到ecx和eax寄存器
rep stos dword ptr es:[edi] : 将edi开始位置的39h个dword字节的空间都改成eax中的值
上述三条指令实际是对main()函数空间的初始化.
c中的int a =10 编译后的指令: 将0Ah(10)放到ebp-8的位置
接下来的两条指令实现的是一样的功能:分别将14h和0放入到ebp-14和ebp-20h的位置:
接下来就是函数调用:
将ebp-14h位置的值加载到eax寄存器
eax压栈
将ebp-8位置的值加载到ecx
ecx压栈
上述四条指令就是在传参.
call 00C210E1,call指令实际上就是将call指令的下一条指令的地址压栈:
这条指令决定了函数被调用完以后如何返回
接下来就来到了Add函数内部:
函数栈帧的准备工作与前面main()函数栈帧的准备工作步骤相同:
1.ebp地址压栈
2.ebp来到esp的位置
3.esp减去一个值,即为函数开辟栈帧空间
4.ebi,esi,edi 三个寄存器压栈,此时esp指向edi位置,edi中的内容是三个寄存器压栈前的地址
5.将edi内位置开始的ecx个dword字节的空间初始化为eax内值的大小
6.开始执行函数内的代码
这里main()函数中a 的值传给x,b的值传给y,在调用Add()函数之前,已经将a,b参数压栈,同时还有call指令的下一条指令的地址和main()函数栈底地址:
mov ,dword ptr [ebp+8]:就是将参数给赋值给eax寄存器
将运算结果返回:
函数内部执行完的结果在ebp-8的位置,这里执行return,就是想刚才的运算结果赋值给eax寄存器,随后函数栈帧空间被销毁.
最后的花括号,出栈三个寄存器,每pop一次,esp++一下,然后将ebp的值赋值给esp,
esp维护的不再是add函数的栈帧空间,注意这里的pop ebp,此处的值是main()函数的栈底地址,pop给ebp,那么ebp就来到了main()函数栈底的位置.
这里的ret:在形参压栈完之后,我们将call指令的下一条指令的地址压栈,此时ret返回的就是call的下一条指令
通过上文,希望你能够大致了解,函数栈帧是如何创建的,参数是怎么传递的,以及函数调用完之后是如何返回的.