C语言程序的存储区域和内存布局

来源:互联网 发布:乔治华盛顿大学 知乎 编辑:程序博客网 时间:2024/05/14 00:33



《嵌入式Linux上的C语言编程实践》第13韩超/魏治宇/廖文江 电子工业出版社

段式内存管理架构

BSS:BlockStarted by Symbol

 

13.1 C语言程序的存储区域

本章介绍C语言程序的内存布局结构,包括连接过程中目标程序各个段的组成和运行过程中各个段加载的情况。

在本章的学习中,读者应重点关注以下内容:

C语言程序在内存中各个段的组成

C语言程序连接过程中的特性和常见错误

C语言程序的运行方式

13.1 C语言程序的存储区域

由C语言代码(文本文件)形成可执行程序(二进制文件),需要经过编译-汇编-连接三个阶段。编译过程把C语言文本文件生成汇编程序,汇编过程把汇编程序形成二进制机器代码,连接过程则将各个源文件生成的二进制机器代码文件组合成一个文件。

C语言编写的程序经过编译-连接后,将形成一个统一文件,它由几个部分组成。在程序运行时又会产生其他几个部分,各个部分代表了不同的存储区域:

1.代码段(Code或Text)

代码段由程序中执行的机器代码组成。在C语言中,程序语句进行编译后,形成机器代码。在执行程序的过程中,CPU的程序计数器指向代码段的每一条机器代码,并由处理器依次运行。

2.只读数据段(RO data)

只读数据段是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要更改,因此只需要放置在只读存储器中即可。

3.已初始化读写数据段(RW data)

已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并具有初值,以供程序运行时读写。

4.未初始化数据段(BSS)

未初始化数据是在程序中声明,但是没有初始化的变量,这些变量在程序运行之前不需要占用存储器的空间。

5.堆(heap)

堆内存只在程序运行时出现,一般由程序员分配和释放。在具有操作系统的情况下,如果程序没有释放,操作系统可能在程序(例如一个进程)结束后回收内存。

6.栈(stack)

栈内存只在程序运行时出现,在函数内部使用的变量、函数的参数以及返回值将使用栈空间,栈空间由编译器自动分配和释放。

C语言目标文件的内存布局如图13-1所示。

代码段、只读数据段、读写数据段、未初始化数据段属于静态区域,而堆和栈属于动态区域。代码段、只读数据段和读写数据段将在连接之后产生,未初始化数据段将在程序初始化的时候开辟,而堆和栈将在程序的运行中分配和释放。

C语言程序分为映像和运行时两种状态。在编译-连接后形成的映像中,将只包含代码段(Text)、只读数据段(RO Data)和读写数据段(RW Data)。在程序运行之前,将动态生成未初始化数据段(BSS),在程序的运行时还将动态形成堆(Heap)区域和栈(Stack)区域。

一般来说,在静态的映像文件中,各个部分称之为节(Section),而在运行时的各个部分称之为段(Segment)。如果不详细区分,可以统称为段。

知识点:C语言在编译和连接后,将生成代码段(Text)、只读数据段(RO Data)和读写数据段(RW Data)。在运行时,除了以上三个区域外,还包括未初始化数据段(BSS)区域和堆(Heap)区域和栈(Stack)区域。



13.2 C语言程序的段

13.2.1 段的分类

根据C语言的特点,每一个源程序生成的目标代码将包含源程序所需要表达的所有信息和功能。目标代码中各段生成情况如下:

1.代码段(Code)

代码段由程序中的各个函数产生,函数的每一个语句将最终经过编译和汇编生成二进制机器代码(具体生成哪种体系结构的机器代码由编译器决定)。

顺序代码

基本数学运算(+,-),逻辑运算(&&,||),位运算(&,|,^)等都属于顺序代码。

选择代码

if,if…else语句等将由编译器生成选择代码。

循环代码

while(),do…while()语句等将由编译器生成循环代码。

对于一些较为复杂的数学运算如除法(\),取余(%)等,虽然它们是C语言的基本运算,但在各种编译系统中的处理方式却不一定相同。根据编译器和体系结构的特点,对它们的处理方式有可能与加减等运算相同,即直接生成处理器的机器代码,也有可能转换成一个库函数的调用。例如,在没有除法指令的体系结构中,编译器在编译a/b这类除法运算的时候,由于处理器没有与其对应的指令,因此会使用调用库函数来模拟除法运算。浮点数的处理与之类似:对于支持浮点运算的体系结构,将直接生成浮点代码;对于不支持浮点数的处理器,编译器将会把每一个浮点运算用库函数调用的方式模拟。

2.只读数据段(RO Data)

只读数据段由程序中所使用的数据产生,该部分数据的特点是在运行中不需要改变,因此编译器会将该数据放入只读的部分中。C语言的一些语法将生成只读数据段。

只读全局量

例如:定义全局变量const char a[100]={"ABCDEFG"}将生成大小为100个字节的只读数据区,并使用字串"ABCDEFG"初始化。如果定义为const char a[]={"ABCDEFG"},没有指定大小,将根据"ABCDEFG"字串的长度,生成8个字节的只读数据段。

只读局部量

例如:在函数内部定义的变量const char b[100]={"9876543210"};其初始化的过程和全局量一样。

程序中使用的常量

例如:在程序中使用printf("information \n"),其中包含了字串常量,编译器会自动把常量"information \n"放入只读数据区。

在const char a[100]={"ABCDEFG"}中,定义了100个字节的数据区,但是只初始化了前面的8个字节(7个字符和表示结束的'\0')。在这种用法中,实际后面的字节没有初始化,但是在程序中也不能写,实际上没有任何用处。因此,在只读数据段中,一般都需要做完全的初始化。

3.读写数据段(RW Data)

读写数据段表示了在目标文件中一部分可以读也可以写的数据区,在某些场合它们又被称为已初始化数据段。这部分数据段和代码段,与只读数据段一样都属于程序中的静态区域,但是具有可写的特点。

已初始化全局静态变量

例如:在函数外部,定义全局的变量char a[100]={"ABCDEFG"}

已初始化局部静态变量

例如:在函数中定义static char b[100]={"9876543210"}。函数中由static定义并且已经初始化的数据和数组将被编译为读写数据段。

读写数据区的特点是必须在程序中经过初始化,如果只有定义,没有初始值,则不会生成读写数据区,而会定位为未初始化数据区(BSS)。如果全局变量(函数外部定义的变量)加入static修饰符,写为类似static char a[100]的形式,这表示只能在文件内使用,而不能被其他文件使用。

4.未初始化数据段(BSS)

未初始化数据段常被称之为BSS(英文BlockStart by Symbol的缩写)。与读写数据段类似,它也属于静态数据区,但是该段中的数据没有经过初始化。因此它只会在目标文件中被标识,而不会真正称为目标文件中的一个段,该段将会在运行时产生。未初始化数据段只有在运行的初始化阶段才会产生,因此它的大小不会影响目标文件的大小。

 

    只有堆和栈算是动态数据区??

 

在C语言的程序中,对变量的使用还有以下几点需注意:

1.在函数体中定义的变量通常是在栈上,不需要在程序中进行管理,由编译器处理。

2.用malloc,calloc,realloc等分配内存的函数所分配的内存空间在堆上,程序必须保证在使用后用free释放,否则会发生内存泄漏。

3.所有函数体外定义的是全局变量,加了static修饰符后的变量不管在函数内部或者外部都存放在全局区(静态区)。

4.使用const定义的变量将放于程序的只读数据区。

在C语言中,可以定义static变量:在函数体内定义的static变量只能在该函数体内有效;在所有函数体外定义的static变量,也只能在该文件中有效,不能在其他的源文件中使用;对于没有使用static修饰的全局变量,可以在其他的源文件中使用。这些区别是编译的概念,即如果不按要求使用变量,编译器会报错。使用static和没有使用static修饰的全局变量最终都将放置在程序的全局区(静态区)。

 

 

 

 

 

 

 

13.2.2 程序中段的使用

本小节使用简单的例子,说明C语言中变量和段的对应关系。C语言程序中的全局区(静态区),实际对应着下述几个段:

只读数据段:RO Data

读写数据段:RW Data

未初始化数据段:BSS Data

一般来说,直接定义的全局变量在未初始化数据区,如果该变量有初始化则是在已初始化数据区(RWData),加上const修饰符将放置在只读区域(ROData)。

示例1:

const char ro[]={"this is readonly data"}; /* 只读数据段 */
static char rw1[]={"this is global readwrite data"}; 
 /* 已初始化读写数据段 */
char bss_1[100];       /* 未初始化数据段 */
const char* ptrconst = "constant data"; /* "constant data"放在只读数据段 */
int main()
{
short b;      /* b放置在栈上,占用2个字节 */
char a[100];    /* 需要在栈上开辟100个字节,a的值是其首地址 */
char s[] = "abcde";   /* s在栈上,占用4个字节 */
/* "abcde "本身放置在只读数据存储区,占6字节 */
char *p1;      /* p1在栈上,占用4个字节  */
char *p2 = "123456";   /* "123456"放置在只读数据存储区,占7字节 */
  /* p2在栈上,p2指向的内容不能更改。 */
static char rw2[]={"this is local readwrite data"};  
 /* 局部已初始化读写数据段 */ 
static char bss_2[100];  /* 局部未初始化数据段 */ 
static int c = 0;    /* 全局(静态)初始化区 */
p1= (char *)malloc(10*sizeof(char));
/* 分配的内存区域在堆区。 */
strcpy(p1, "xxxx");   /* "xxxx"放置在只读数据存储区,占5字节 */
free(p1);     /* 使用free释放p1所指向的内存 */
return 0;
}

示例1程序中描述了C语言源文件中语句如何转换成各个段。

只读数据段需要包括程序中定义的const型的数据(如:constchar ro[]),还包括程序中需要使用的数据如"123456"。对于const char ro[]和const char* ptrconst的定义,它们指向的内存都位于只读数据区,其指向的内容都不允许修改。区别在于前者不允许在程序中修改ro的值,后者允许在程序中修改ptrconst本身的值。对于后者,改写成以下的形式,将不允许在程序中修改ptrconst本身的值:

const char* const ptrconst = "constant data";

读写数据段包含了已经初始化的全局变量staticchar rw1[]以及局部静态变量static char rw2[]。rw1和rw2的差别只是在于编译时,是在函数内部使用的还是可以在整个文件中使用。对于前者,static修饰在于控制程序的其他文件是否可以访问rw1变量,如果有static修饰,将不能在其他的C语言原文件中使用rw1,这种影响针对编译-连接的特性,但无论有无static,变量rw1都将被放置在读写数据段。对于后者rw2,它是局部的静态变量,放置在读写数据区;如果不使用static修饰,其意义将完全改变,它将会是开辟在栈空间局部变量,而不是静态变量。在这里,rw1和rw2后面的方括号([])内没有数值,表示静态区的大小由后面字符串的长度决定,如果将它们的定义改写为以下形式:

static char rw1[100]={"that is global readwrite data"};
static char rw2[200]={"that is local readwrite data"};

则读写数据区域的大小直接由后面的数值(100或者200)决定,超出字符串长度的空间的内容为0。

未初始化数据段,示例1中的bss_1[100]和bss_2[200]在程序中代表未初始化的数据段。其区别在于前者是全局的变量,在所有文件中都可以使用;后者是局部的变量,只在函数内部使用。未初始化数据段不设置后面的初始化数值,因此必须使用数值指定区域的大小,编译器将根据大小设置BBS中需要增加的长度。

栈空间包括函数中内部使用的变量如short b和chara[100],以及char *p1中p1这个变量的值。

变量p1指向的内存建立在堆空间上,栈空间只能在程序内部使用,但是堆空间(例如p1指向的内存)可以作为返回值传递给其他函数处理。示例1中各段的使用如表13-1所示。

 

 

 

 

 

表13-1  示例1中各段的使用

使用情况

列    表

只读数据(RO data)

程序中不需要更改的全局变量

const char ro[]={"it is readonly data"};

char s[] = "abcde";中的"abcde",占用6字节

char *p2 = "123456";中的"123456",占用7字节

strcpy(p1, "xxxx");中的"xxxx",占用5字节

读写数据(RW data)

程序中需要更改,但是具有初始化值的全局变量

static char rw1[]={"that is global readwrite data"};   

static char rw2[]={"that is local readwrite data"};

未初始化数据(BSS)

没有初始化值的全局变量

char bss_1[100];   

char bss_2[100];

堆(heap)

需要用户分配的空间

p1= (char *)malloc(10*sizeof(char));

p1指向的内存空间

栈(stack)

函数中临时变量

函数入口和返回值

函数中的变量:

short b;        

char a[100];   

char *p1; 变量p1本身占用4字节

示例2:

long fun(long c)
{
char a[100];                                /* 栈上的100个字节 */
long b;                                      /* 栈上的4个字节 */

/* ………… */
return b;
}

栈空间主要用于以下3数据的存储:

函数内部的动态变量

函数的参数

函数的返回值

栈空间主要的用处是供函数内部的动态变量使用,变量的空间在函数开始之前开辟,在函数退出后由编译器自动回收。

栈空间的另外一个作用是在程序进行函数调用的时候进行函数的参数传递以及保存函数返回值。

在示例2程序的调用过程中,需要使用栈保存临时变量。在调用之前,需要将变量C保存在堆栈上,占用4字节,在函数返回之前,需要将返回值b保存在栈上,也占用4个字节的空间。同时,为数组a开辟100字节的栈空间。

在函数的调用过程中,如果函数调用的层次比较多,所需要的栈空间也逐渐增大。对于参数的传递和返回值,如果使用较大的结构体,在使用的栈空间也会比较大。

 

 

 

 

 


0 0
原创粉丝点击