分析以下代码中变量存储空间如何分配:
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 ", pszChar, pszChar, rVarInitNonZero); 35 free(pszChar); 36 free(pszCharNext); 37 38 return 0; 39 }
编译生成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) "及"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 (ebp-0x14)。
栈区从高地址向低地址增长,但数组则从低地址向高地址排列,数组元素szStrInited[n]的地址 = 数组基地址(szStrInited做右值即表示基地址) + n × 每个元素的字节数。当n=0时,元素szStrInited[0]的地址即为数组基地址,因此数组下标从0开始。
变量rVarInitNonZero并未在栈上分配存储空间,而是直接存入ebx寄存器,后面调用printf直接从ebx寄存器里取出变量值当作参数压栈。因此,register关键字用于指示编译器尽可能分配一个寄存器来存储该变量。此外,调用printf时对格式化参数"pszChar: %p(%s), rVarInitNonZero:%d "压栈的是其.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编程一站式学习》一书相应章节,在此致谢!