首先来了解一下,代码在内存中的分配,咱们先用图来看一下:
地址分配图很清楚的可以看出,代码在内从中分布。
其中要注意几点:
代码区具有只读性,文字常量也只具有只读性。
静态区又叫全局变量区,生命周期是文件的生命周期。
最主要的是栈和堆,栈的空间是系统管理,用时自动申请,不用时自动销毁。而堆是由程序员自己管理,自己申请自己销毁,如果不销毁就会产生内存泄漏。
接下来主要分析函数在栈中的调用,即函数的栈帧:
在了解函数栈帧之前,我们先得了解计算cpu中的几个寄存器。
1.EBP叫基址指针寄存器,用于指向栈底。
2.ESP叫指针寄存器也是EBP的一种,用于指向栈顶。
3.EIP/IP/PC叫指令寄存器,用来保存当前指令的下一条指令的地址。
还有很多寄存器,像EAX,EBX,ECX,EDX,DI 等,现在只需要知道这几个寄存器
我们还得知道:
代码的执行过程,在我们看来代码的入口就是main()函数,但是main()函数也是被其它函数调用,可以在VS2008中打开栈调用过程,可以看见调用main()函数的是mainCRTStarup()函数。
这里咱们就自己的逻辑入口main()函数起,来咱们用图来解释一下函数在栈中的生成调用。
上面是函数的栈帧结构从main()函数的开始。
要研究函数栈帧,咱们先用一段代码来演示:
#include<stdio.h>
int function(int x,int y)
{
return x+y;
}
int main()
{
int a = 0x66666666;
int b = 0x99999999;
int c = function(a,b);
printf("finish off\n");
return 0;
}
在栈中先定义的地址高,因为栈是向下生长的,所以如上图。
当执行到
int c = function(a,b);
这句代码,它先跳转到function()这个函数,这时就用到了在汇编中的一个助记符call。
这里还需要了解到,函数对应的汇编过程:
int main()
{
010213F0 push ebp
010213F1 mov ebp,esp
010213F3 sub esp,0E4h
010213F9 push ebx
010213FA push esi
010213FB push edi
010213FC lea edi,[ebp-0E4h]
01021402 mov ecx,39h
01021407 mov eax,0CCCCCCCCh
0102140C rep stos dword ptr es:[edi]
int a = 0x66666666;
0102140E mov dword ptr [a],66666666h
int b = 0x99999999;
01021415 mov dword ptr [b],99999999h
int c = function(a,b);
0102141C mov eax,dword ptr [b]
0102141F push eax
01021420 mov ecx,dword ptr [a]
01021423 push ecx
01021424 call @ILT+165(_function) (10210AAh)
01021429 add esp,8
0102142C mov dword ptr [c],eax
printf("finish off\n");
0102142F mov esi,esp
01021431 push offset string "finish off\n" (102573Ch)
01021436 call dword ptr [__imp__printf (10282BCh)]
0102143C add esp,4
0102143F cmp esi,esp
01021441 call @ILT+315(__RTC_CheckEsp) (1021140h)
return 0;
当用到CALL后IP寄存器会将function()函数地址保存起来,然后CALL跳转到function()函数中。当函数执行完了,想要返回main()函数中,所以在CALL时候会把下一条指令的地址压栈。
然后在在栈中开辟新空间,并把对应的形参实例化,而在实例化过程中是从右向左实例化。正如汇编中的执行语句。
如图:
上面所说的,
当函数执行完了,想要返回main()函数中,所以在CALL时候会把下一条指令的地址压栈。
所以,有
最后将EBP中地址地址压入栈中,然后再进行EBP,ESP指针位置变换,开辟空间,并计算表达式如图:
这样后就会执行完成,然后做得是,函数返回,把计算结果放到eax寄存器后,又出栈,因为栈是先进后出,所以已原来的方法回退回去。
变成了:
所以就完成了函数调用,这就是函数的栈帧。