我们拿一个简单的程序来看一看。
在底层汇编之下函数的调用和内存的关系。
#include<stdio.h>
int add(int a,int b)
{
int c=0;
c=a+b;
return c;
}
void main()
{
int a=10;
int b=20;
int z=0;
z=add(a,b);
printf("%d",z)
3: {
008E13A0 push ebp
008E13A1 mov ebp,esp
008E13A3 sub esp,0CCh
008E13A9 push ebx
008E13AA push esi
008E13AB push edi
008E13AC lea edi,[ebp-0CCh]
008E13B2 mov ecx,33h
008E13B7 mov eax,0CCCCCCCCh
008E13BC rep stos dword ptr es:[edi]
4: int c=0;
008E13BE mov dword ptr [c],0
5: c=a+b;
008E13C5 mov eax,dword ptr [a]
008E13C8 add eax,dword ptr [b]
008E13CB mov dword ptr [c],eax
6: return c;
008E13CE mov eax,dword ptr [c]
7: }
008E13D1 pop edi
008E13D2 pop esi
008E13D3 pop ebx
008E13D4 mov esp,ebp
008E13D6 pop ebp
008E13D7 ret
这是add函数的一部分,我们看出,先是要保留ebp里面的内容,,然后把一会可能用到的esi,edi,ebx压入到栈里面,然后分配给次函数栈帧,大小就是十六进制的cch,对其进行调试信息,将所分配的空间全赋值为cc,在VC之下我们可能经常看到的是“烫烫烫烫”,这是其对应的中文汉字,但这不是绝对的,有的编译器会出现“凸凸凸”的情形。随后计算之后,结果就使用eax返回,
你可能会问,那结果多于eax的字长那?
如果多余eax的字长,系统会使eax和ebx两个寄存器来返回结果,在eax里面放低位的值,在ebx里面放高位的值。
然后我们就将原来的寄存器的内容回复并恢复esp和ebp的内容。
但是,在函数调用后要将其传入的参数也释放掉,这就用到了函数的调用惯例,所谓的函数的调用惯例就是,传入参数的顺序,是从左还是从右,释放时是调用函数释放,还是函数自己本身释放。 就比如你要和一个外国人交流,要么他说你的语言,要么你说他的语言。
在c语言里面的默认调用方式是cdecl,也就是参数从左向右,被调用者释放的一种惯例。而且其名字修饰是在函数前面加上下划线。(所谓名字修饰就是为了在链接的时候对调用惯例进行区分)。
另外还存在着stdcall,此方式的调用不同于上面的是函数自己释放,等等。
栈一般的增长方式是向下增长的。
那为什么要引进堆那?
因为栈里面的变量的生存期都受到了函数的影响,调用函数不能对一个已经“死亡”的变量进行应用。但又人会说那我定义个全局变量就行了么。但全局变量的灵活性太差。 于是引进了堆。
堆的实现机制,其实就是向运行库申请一块内存,然后自己去管理。运行库就像超市的“物品”一样,应用进程去“超市”采购,若有,就卖给他,没有,运行库就通过API向系统申请更大的内存。
现在大家清楚了 malloc其实就相当一个运行库,他包装了4个函数,有,创建,申请,撤销,摧毁。