C语言程序内存布局
C语言程序内存布局
如有转载,请注明出处:http://blog.csdn.net/embedded_sky/article/details/44457453
作者:super_bert@csdn
一、几个概念
1.栈(Stack)
C语言通过栈来维护函数调用上下文,也就是说C中的函数、函数参数列表、局部变量、函数返回值都保存在栈内存中,在完成函数调用之后栈帧随即销毁,至于具体的压栈顺序和上下文维护工作由谁来完成,则取决于函数的调用方式(cdecl/stdcall/fastcall/pascal).
关于C函数调用约定,或者叫调用惯例(Calling Convention),以下表格中,个人认为出栈方应该叫做栈的维护方更确切,表格:
栈帧一般包括如下几方面内容:
①函数的参数列表和返回值(返回地址);
②临时变量:包括非静态局部变量和编译器自动生成的其他临时变量;
③保存的上下文:包括函数调用前后需保持不变的寄存器。
栈增长示意图
2.堆(Heap)
堆空间是需要程序自行申请的,简言之 在C语言程序中对空间由malloc函数族(malloc/alloc/calloc/realloc)申请,当然在C++中还有new关键字。堆空间由程序猿自行申请,也需由程序猿自行释放(此处,Javaer们笑看你们打脸)。malloc/free new/delete分别对应申请和释放内存,new/delete是C++中的关键字,不是函数。
通常情况下比较好的做法是,free掉之后,立即将指针置为NULL,因为通常if语句只会去判断指针是否为NULL(0x00000000),堆空间(内存)在free掉之后,该指针并不会指向NULL,也就是说free掉的是内存而不是指针,如果程序之后又用到该指针,后果将是不可预知的。如果被置为NULL,则可以被if语句检测出来,防止自己给自己留坑,C中的坑已经够多的了。
glibc中的malloc函数是这样处理用户堆空间请求的:
①对于小于128KB的请求,在现有的堆空间里,按照分配算法为他分配一块空间并返回;
②对于大于128KB的请求,malloc内部会调用mmap()函数分配一块匿名空间,然后在这块匿名空间中为用户分配空间。
mmap函数的具体用法请参考Linux manpage,Windows环境下是没有mmap函数的,malloc函数族应该是依赖于 VirtualAlloc()函数申请堆空间。
3.动态链接库(Dynamic Libraries)
Linux/Unix环境下应该是***.so之类的文件,Windows下则是DLL(Dynamic Linking Libraries)。
二、Linux进程地址空间
先上图,箭头代表stack和heap的增长方向,dynamic libraries在2.4和2.6以后的内核中装载位置不一样,分别为0x40000000,0xBFXXXXXX.
Linux采用了分页的内存管理机制。由于x86体系的分页机制是基于分段机制的,因此,为了使用分页机制,分段机制是无法避免的。为了降低复杂性,Linux内核将所有段的基址都设为0,段限长设为4G,只是在段类型和段访问权限上有所区分,并且Linux内核和所有进程共享1个GDT,不使用LDT(即系统中所有的段描述符都保存在同一个GDT中),这是为了应付CPU的分段机制所能做的最少工作。
Linux内存管理机制可以分为3个层次,从下而上依次为物理内存的管理、页表的管理、虚拟内存的管理。
三、Windows进程地址空间
相较Linux而言,Windows环境下的进程空间布局就显得"支离破碎",
各位看官么看完这图估计会有点奇怪,一开始我也奇怪,为啥会有那么多stack呢?上面有说到,栈用于维护函数调用上下文,一个进程中调用函数所需存储的参数/变量/返回值,哪用得了那么多栈内存。
造成需要这么多栈内存的原因如下,每个线程的栈空间都是独立的,而Windows程序一般都是多线程,很大一部分原因Windows的进程耗费资源太大,线程优化得比*nix要好得多。试想一下,若在一个进程中通过CreateThread创建了多个线程,然后每个线程的栈空间都是独立的,且可以通过CreateThread API的参数来指定(系统默认的每个线程栈大小为1M byte),所以一旦程序起起来,内存空间已经支离破碎了。
然而我们的应用程序得在这个支离破碎的内存空间中去malloc堆(heap)空间,VirtualAlloc().
malloc函数族在Windows环境下最终依赖于VirtualAlloc()函数实现,而VirtualAlloc()函数像操作系统申请的空间必须是页大小的整数倍,也就是说对于x86系统一次性申请的heap最少是4096个byte,OS就是这么定的。但是OS还提供了一个堆管理器,堆管理器每次向OS申请一大片heap空间,然后在申请到的空间里给应用程序进行malloc。
HeapCreate创建一个堆;
HeapAlloc从堆空间中申请内存;
HeapFree释放内存;
HeapDestroy销毁一个堆;
相当于咱们从冷库里一次性批发了一箱冰激凌放在家里,咱们想吃的时候就一个一个去冰箱拿,想吃几个就拿几个。
此文只是抛砖引玉,其中很多地方都值得深入去研究、理解。
参考:http://www.cnblogs.com/zszmhd/archive/2012/08/29/2661461.html
x64系统程序内存布局请参考:http://blog.chinaunix.net/uid-27119491-id-3325943.html
-------------------------------------------------------
calloc(), malloc(), realloc(), free(),alloca()
内存区域可以分为栈、堆、静态存储区和常量存储区,局部变量,函数形参,临时变量都是在栈上获得内存的,它们获取的方式都是由编译器自动执行的。
利用指针,我们可以像汇编语言一样处理内存地址,C 标准函数库提供了许多函数来实现对堆上内存管理,其中包括:malloc函数,free函数,calloc函数和realloc函数。使用这些函数需要包含头文件stdlib.h。
四个函数之间的有区别,也有联系,我们应该学会把握这种关系,从而编出精炼而高效的程序。
在说明它们具体含义之前,先简单从字面上加以认识,前3个函数有个共同的特点,就是都带有字符”alloc”,就是”allocate”,”分配”的意思,也就是给对象分配足够的内存,” calloc()”是”分配内存给多个对象”,” malloc()”是”分配内存给一个对象”,”realloc()”是”重新分配内存”之意。”free()”就比较简单了,”释放”的意思,就是把之前所分配的内存空间给释放出来。
void *calloc(size_t nobj, size_t size);
分配足够的内存给nobj个大小为size的对象组成的数组, 并返回指向所分配区域的第一个字节的指针;
若内存不够,则返回NULL. 该空间的初始化大小为0字节.
char *p = (char *) calloc(100,sizeof(char));
void *malloc(size_t size);
分配足够的内存给大小为size的对象, 并返回指向所分配区域的第一个字节的指针;
若内存不够,则返回NULL. 不对分配的空间进行初始化.
char *p = (char *)malloc(sizeof(char));
void *realloc(void *p, size_t size);
将p所指向的对象的大小改为size个字节.
如果新分配的内存比原内存大, 那么原内存的内容保持不变, 增加的空间不进行初始化.
如果新分配的内存比原内存小, 那么新内存保持原内存的内容, 增加的空间不进行初始化.
返回指向新分配空间的指针; 若内存不够,则返回NULL, 原p指向的内存区不变.
char *p = (char *)malloc(sizeof(char));
p= (char *)realloc(p, 256);
void free(void *p);
释放p所指向的内存空间; 当p为NULL时, 不起作用.
p必先调用calloc, malloc或realloc.
值得注意的有以下5点:
(1)通过malloc函数得到的堆内存必须使用memset函数来初始化
malloc函数分配得到的内存空间是未初始化的。因此,一般在使用该内存空间时,要调用另一个函数memset来将其初始化为全0,memset函数的声明如下:void * memset (void * p,int c,int n) ;
该函数可以将指定的内存空间按字节单位置为指定的字符c,其中,p为要清零的内存空间的首地址,c为要设定的值,n为被操作的内存空间的字节长度。如果要用memset清0,变量c实参要为0。
malloc函数和memset函数的操作语句一般如下:
int * p=NULL;
p=(int*)malloc(sizeof(int));
if(p==NULL)
printf(“Can’t get memory!\n”);
memset(p,0,siezeof(int));
(2)使用malloc函数分配的堆空间在程序结束之前必须释放
从堆上获得的内存空间在程序结束以后,系统不会将其自动释放,需要程序员来自己管理。一个程序结束时,必须保证所有从堆上获得的内存空间已被安全释放,否则,会导致内存泄露。
我们可以使用free()函数来释放内存空间,但是,free函数只是释放指针指向的内容,而该指针仍然指向原来指向的地方,此时,指针为野指针,如果此时操作该指针会导致不可预期的错误。安全做法是:在使用free函数释放指针指向的空间之后,将指针的值置为NULL。
(3)calloc函数的分配的内存也需要自行释放
calloc函数的功能与malloc函数的功能相似,都是从堆分配内存,它与malloc函数的一个显著不同时是,calloc函数得到的内存空间是经过初始化的,其内容全为0。calloc函数适合为数组申请空间,可以将size设置为数组元素的空间长度,将n设置为数组的容量。
(4)如果要使用realloc函数分配的内存,必须使用memset函数对其内存初始化
realloc函数的功能比malloc函数和calloc函数的功能更为丰富,可以实现内存分配和内存释放的功能。realloc 可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变。当然,对于缩小,则被缩小的那一部分的内容会丢失。realloc 并不保证调整后的内存空间和原来的内存空间保持同一内存地址。相反,realloc 返回的指针很可能指向一个新的地址。
所以,在代码中,我们必须将realloc返回的值,重新赋值给 p :
p = (int *) realloc(p, sizeof(int) *15);
甚至,你可以传一个空指针(0)给 realloc ,则此时realloc 作用完全相当于malloc。
int* p = (int *)realloc (0,sizeof(int) * 10); //分配一个全新的内存空间,
这一行,作用完全等同于:
int* p = (int *)malloc(sizeof(int) * 10);
(5)关于alloca()函数
还有一个函数也值得一提,这就是alloca()。其调用序列与malloc相同,但是它是在当前函数的栈帧上分配存储空间,而不是在堆中。其优点是:当函数返回时,自动释放它所使用的栈帧,所以不必再为释放空间而费心。其缺点是:某些系统在函数已被调用后不能增加栈帧长度,于是也就不能支持alloca 函数。尽管如此,很多软件包还是使用alloca函数,也有很多系统支持它。
总结:应用时候需要记得,只有calloc可以指定个数和大小,而且能够对分配内存进行初始化,其余函数均不会对内存进行初始化工作,需要自行调用memset()函数.
============== End