C语言变量的存储布局

 

     分析以下代码中变量存储空间如何分配: 

 1 //MemSeg.c: 代码无意义,仅供分析用
 2 #include <stdio.h>
 3 #include <stdlib.h>  //malloc函数声明位于<stdlib.h>或<cstdlib>头文件中
 4 #include <string.h>
 5 
 6 #define LEN_TEST_INT        4
 7 #define LEN_TEST_CHAR       20
 8 
 9 int gBufInitZero[LEN_TEST_INT] = {0};
10 int gBufInitNonZero[LEN_TEST_INT] = {0xA, 0xB, 0xC, 0xD};
11 int gBufUnInit[LEN_TEST_INT];
12 
13 const int gcVarInitNonZero = 10;  //const变量定义时必须初始化,因为定义后再不能改
14 int gVarInitZero = 0;
15 int gVarInitNonZero = 20;
16 int gVarInitUnInit;
17 static int gsVarInitZero = 0;
18 static int gsVarInitNonZero = 30;
19 static int gsVarUnInit;
20 
21 char gszStrInited[] = "STR_INITED";
22 char *gpszStrInited = "PSZ_INITED";
23 
24 int main(void){
25     static int sVarInitNonZero = 40;
26     char szStrInited[] = "Hello World";
27     register int rVarInitNonZero = 50;
28     
29     char *pszChar = (char *)malloc(LEN_TEST_CHAR);  //为简便起见未考虑内存分配错误处理
30     char *pszCharNext = (char *)malloc(LEN_TEST_CHAR);
31     
32     memset(pszChar, 0, LEN_TEST_CHAR);
33     strcpy(pszChar, szStrInited);
34     printf("pszChar: %p(%s), rVarInitNonZero:%d\n", pszChar, pszChar, rVarInitNonZero);
35     free(pszChar);
36     free(pszCharNext);
37 
38     return 0;
39 }
MemSeg

 

     编译生成a.out文件(gcc MemSeg.c -g),通过readelf命令(readelf -a a.out)查看目标文件中各变量的地址分布。截图中为突出重点对符号表结果进行了重排和截取:

 

     图中,Num列是符号表序号(已重新排列)。Value列是符号地址,对于可重定位目标文件表示距定义目标文件的节(Section)起始位置的偏移,对于可执行目标文件表示绝对运行地址。Size列为符号字节大小。Type列指示数据(OBJECT)、函数(FUNC)或节(SECTION)等类型。Bind列表明符号的绑定信息,分为局部(对于目标文件的外部不可见)、全局(外部可见)和弱引用(WEAK)。Ndx列为数字时表示节索引,为ABS时代表不该被重定位的符号(如文件名),UND代表定义在别处的符号(如printf库函数),COM代表未初始化数据目标(如某些编译器的未初始化全局变量)。Name列为符号名(图中所示为变量名)。

     全局变量gcVarInitNonZero用const修饰,表明不应修改其值。该变量被分配0x80485b0地址,从readelf输出可知该地址位于.rodata段(局部只读变量位于栈区):

 

     该段在文件中的地址是0x5a4~0x5f1(0x5a4+0x4d)。用hexdump命令查看该段内容:

 

     其中0x5a0地址处的0a 00 00 00(小字节序)即变量gcVarInitNonZero的值。也可看到字符串字面值"PSZ_INITED"、"pszChar: %p(%s)\n"及"Hello World"分配在.rodata段。

     .data段从地址0x80496fc开始,长度是0x30,即到地址0x804972c结束。.data段存放初值不为0的非const全局变量或静态局部变量。gVarInitNonZero是个GLOBAL符号,而gsVarInitNonZero被static关键字修饰而成为LOCAL符号(不被链接器处理,即不能被其他文件引用)。静态局部变量sVarInitNonZero.2443只在函数内起作用,故编译器对其符号名附加后缀,以便与同名的全局变量或其它函数的变量区分。

     注意,若字符数组定义在函数外,则初始化字符串存放在.data段(如"STR_INITED");否则存放在.rodata段(如"Hello World")。——不解???

     .bss段从地址0x804972c开始(紧挨.data段),长度为0x38,即到地址0x8049764结束。.bss段存放未初始化或初值为0的全局变量或静态局部变量,如gVarInitUnInit、gVarInitZero及gsVarUnInit等。

     程序加载运行时,.rodata段和.text段通常合并为一个Segment,操作系统将该Segment页面只读保护起来,防止被意外改写。.data和.bss段在加载时也合并为一个Segment,该Segment可读可写。这点从readelf输出也可看出:

 

     函数的参数和局部变量分配在栈上,故字符数组szStrInited也在栈区。如下反汇编代码所示:

 

     可见,对栈区字符数组赋值时,利用寄存器从全局数据区把字符串拷贝到栈内存中。szStrInited数组的栈上内容从低地址到高地址依次为:Hell(ebp-0x1c)、o wo(ebp-0x18)、rld\0(ebp-0x14)。

     栈区从高地址向低地址增长,但数组则从低地址向高地址排列,数组元素szStrInited[n]的地址 = 数组基地址(szStrInited做右值即表示基地址) + n × 每个元素的字节数。当n=0时,元素szStrInited[0]的地址即为数组基地址,因此数组下标从0开始。

     变量rVarInitNonZero并未在栈上分配存储空间,而是直接存入ebx寄存器,后面调用printf直接从ebx寄存器里取出变量值当作参数压栈。因此,register关键字用于指示编译器尽可能分配一个寄存器来存储该变量。此外,调用printf时对格式化参数"pszChar: %p(%s), rVarInitNonZero:%d\n"压栈的是其.rodata段中的首地址(0x80485c0),并未压入整个字符串。故字符串使用时可视为数组名,若作为右值则表示数组首元素地址(即指向数组首元素的指针)。

     pszChar和pszCharNext指针本身在栈上分配空间,但却指向malloc动态分配的堆内存。因为堆空间从低地址向高地址增长,故pszCharNext>pszChar(堆),而&pszCharNext<&pszChar(栈)。

     最后,main函数的汇编指令存放在.text段。

 

【其他工具】

     通过size命令可查看ELF文件各段占用的字节空间:

 

     其中,dec和hex项分别为代码段、数据段和BSS段以十进制和16进制表示总字节数。

     通过nm工具可查看按段分布的变量存储信息(比readelf结果易读):

 

     通过objdump工具也可方便地查看目标文件中.rodata和.data段的内容(.bss段不写入目标文件):

 

     objdump -t a.out的输出结果与nm –Ans类似。

     此外,通过反汇编文件MemSeg.s(gcc -S MemSeg.c)也可查看.bss、.data和.text及栈区所存放的变量。

 

注:本文参考《Linux C编程一站式学习》一书相应章节,在此致谢!

 

posted @ 2014-05-27 14:35  clover_toeic  阅读(2107)  评论(0编辑  收藏  举报