C程序的内存管理
熟悉Java语言的肯定知道,Java中内存管理是由虚拟机帮助我们完成的,在C/C++中可不是这样,程序员需要自己去分配和回收内存空间。本文记录了C程序可执行文件的存储结构、在内存中的存储结构等方面的内容。以下C程序所使用的编译器版本是GCC 4.4.7。
从一个C程序说起
文件的结构
对于以下这段Hello.c程序再熟悉不过了
#include<stdio.h>
int main(void)
{
printf("Hello World\n");
return 0;
}
下面使用gcc编译它,然后运行可执行文件,再查看可执行文件的存储结构
可以看出,可执行文件Hello在存储时(没有调入内存时)分为代码区(text),数据区(data)和未初始化数据区(bss)3个部分。另外3个字段中,dec表示十进制总和,hex表示十六进制总和,filename表示文件名。各段的具体说明如下:
(1)代码段(text segment):存放CPU执行的机器指令。通常代码区是可以共享的(即另外的执行程序可以调用它)。代码区通常是只读的,以防止程序意外的修改它的指令。常量数据在编译时在代码区分配内存。代码区的指令包括操作码和操作对象(或对象的地址引用)。如果是立即数,就直接包含在代码中;如果是局部数据,将在运行时的栈空间中分配,然后在引用该数据的地址;如果是bss区和数据区,在代码中同样是引用该数据的地址。
(2)全局初始化数据区/静态数据区(initialized data segment/data segment),或者简称数据段:该区域包含了在程序中明确被初始化的全局变量,已经初始化的静态变量(包括全局静态变量和局部静态变量)。需要注意的是,被const声明的变量和字符串常量在代码段中分配内存。这和汇编语言中的数据段的概念是类似的。
(3)未初始化数据区bss(Block Started By Symbol):存储的是未初始化的全局变量和未初始化的静态变量。bss区域的数据在程序执行前会被内核初始化为0或者空指针(NULL),这和栈中的变量是不同的,栈中的变量(局部变量)如果没有初始化就使用,系统会随机分配一个值给它,这是不安全的。
上述这些都是可执行文件的存储结构分析,其实运行时的内存结构和这个十分类似,只不过多了堆内存和栈内存区域,在后面会分析到。下面通过几个例子验证之。
还是以Hello.c程序为例
我们在Hello.c中增加了一句代码,定义一个常量i,通过分析比较,可以发现代码段text区大小增加了4个字节(一个int类型占4个字节),其他区域不变,可知常量是分配在代码段的。
在上述的基础上,在添加一句,定义一个全局变量a,并给它赋值为2,观察各区域变化
通过比较发现,只有数据段的大小增加了4个字节,也证明了明确被初始化的全局变量是被分配在数据区的。静态变量也是一样,可自行证之。
在上述的基础上,我们在定义一个全局变量b,但是这一个不要赋值,观察各区域变化
可以发现,这一次只有bss区域增加4个字节,也证明了未初始化的全局变量是分配在bss区域的。未初始化的静态变量同理,可自行证之。
进程的结构
一个程序执行的时候就表现为一个或者多个进程,其实进程内核的数据结构和上述文件的存储结构很相似,主要是多了堆内存和栈内存区域。主要的布局如下图所示
各部分说明如下:
(1)代码区(text segment):加载的是上述可执行文件的代码段,其加载到内存中的位置由加载器完成。
(2)全局初始化数据区/静态数据区(Data Segment):加载的是上述可执行文件的数据段,位置位于可执行代码段后面,可以是不相连的。在程序运行之初就为数据段申请了空间,程序退出的时候释放空间,其生命周期是整个程序的运行时期。
(3)未初始化数据区(BSS):加载的是上述可执行文件的BSS段,位置在数据段之后,可以不相连。其生命周期和数据段一样。
(4)栈区(Stack):由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中动态的分配和释放,栈区位于BSS后,是向上有限扩展的。
(5)堆区(Heap):用于动态内存分配。位于栈区的后面,是向下有限扩展的。一般由程序员进行分配和释放,若不释放,在程序结束的时候,由OS负责回收。
堆与栈的区别
栈是由编译器在程序运行时分配的内存空间,由操作系统维护(这和Java虚拟机中的栈内存是类似的)。堆是由malloc( )函数(C++中的new)分配内存,内存的管理由程序员手动控制,在C语言中使用free( )函数完成释放(C++中是delete)。堆和栈的主要区别有以下几点:
(1)管理方式不同。程序在运行时,栈由操作系统自动管理,堆由程序员手动管理,堆内存的管理更容易造成内存的泄漏。
(2)空间大小不同。栈是向低地址扩展的(参考上图)是一块连续的内存空间,栈的容量是预先设定好的,如果申请的栈空间大于该预设值,将会出现栈溢出错误。堆是向高地址扩展的,是不连续的内存空间。系统是采用链表管理空闲的内存地址的,且链表的遍历方向是由低地址向高地址的。
(3)产生的碎片不同。在堆中频繁的使用malloc/free(new/delete)势必会再次内存空间的不连续,产生大量的内存碎片,使程序运行效率降低。而在栈内存中,则完全不会存在这样的问题。
(4)扩展方向不同。在x86平台上,堆是向上扩展的,即向内存地址增加的方向;栈是向下扩展的,即向内存地址减小的方向。
和前面一样,下面使用一个例子去验证运行时的存储分布
#include <stdio.h>
#include <malloc.h>
#include <unistd.h>
#include <alloca.h>
extern void afunc(void);
extern etext,edata,end;<span style="white-space:pre"> </span>
int bss_var; //no init globel data must be in bss
int data_var=42; //init globel data must be in data
#define SHW_ADR(ID,I) printf("the %8s\t is at adr:%8x\n",ID,&I); //the macro to printf the addr
int main(int argc,char *argv[])
{
char *p,*b,*nb;
printf("Adr etext:%8x\t Adr edata %8x\t Adr end %8x\t\n",&etext,&edata,&end);
printf("\ntext Location:\n");
SHW_ADR("main",main); //text section function
SHW_ADR("afunc",afunc); //text section function
printf("\nbss Location:\n");
SHW_ADR("bss_var",bss_var); //bss section var
printf("\ndata location:\n");
SHW_ADR("data_var",data_var); //data section var
printf("\nStack Locations:\n");
afunc();
p=(char *)alloca(32); //alloc memory from statck
if(p!=NULL)
{
SHW_ADR("start",p);
SHW_ADR("end",p+31);
}
b=(char *)malloc(32*sizeof(char)); //malloc memory from heap
nb=(char *)malloc(16*sizeof(char));
printf("\nHeap Locations:\n");
printf("the Heap start: %p\n",b);
printf("the Heap end:%p\n",(nb+16*sizeof(char)));
printf("\nb and nb in Stack\n");
SHW_ADR("b",b);
SHW_ADR("nb",nb);
free(b);
free(nb);
}
void afunc(void)
{
static int long level=0; //data section static var
int stack_var; //temp var ,in stack section
if(++level==5)
{
return;
}
SHW_ADR("stack_var in stack section",stack_var);
SHW_ADR("Level in data section",level);
afunc();
}
其中需要说明的,etext、edata和end(可以理解为end of text、end of data和end of bss)是3个外部的全局变量,是跟用户进程有关的虚拟地址,分别标志着代码段的结束、数据段的结束和bss段的结束。
使用gcc编译上述程序,并运行。
通过观察运行时各区域的内存地址,可以得到以下的内存分布图(每一个运行环境,下面的内存地址值会不一样,但对应关系不变)
需要说明的是,各区域之间并不是相连的,各区域在图中的大小,并不代表实际的大小。从上面还可以发现,afunc函数中的静态变量level四次打印的地址是一样的,而局部变量stack_var四次打印的地址各不相同,也就是说静态变量在整个程序的生命周期内只会被加载初始化一次,而且分配在数据段;而局部变量被分配到栈区,其生命周期是当前函数,每一次进入函数都会重新加载初始stack_var。