C语言变量的存储布局
分析以下代码中变量存储空间如何分配:
//MemSeg.c: 代码无意义,仅供分析用
#include <stdio.h>
#include <stdlib.h> //malloc函数声明位于<stdlib.h>或<cstdlib>头文件中
#include <string.h> #define LEN_TEST_INT 4
#define LEN_TEST_CHAR 20 int gBufInitZero[LEN_TEST_INT] = {};
int gBufInitNonZero[LEN_TEST_INT] = {0xA, 0xB, 0xC, 0xD};
int gBufUnInit[LEN_TEST_INT]; const int gcVarInitNonZero = ; //const变量定义时必须初始化,因为定义后再不能改
int gVarInitZero = ;
int gVarInitNonZero = ;
int gVarInitUnInit;
static int gsVarInitZero = ;
static int gsVarInitNonZero = ;
static int gsVarUnInit; char gszStrInited[] = "STR_INITED";
char *gpszStrInited = "PSZ_INITED"; int main(void){
static int sVarInitNonZero = ;
char szStrInited[] = "Hello World";
register int rVarInitNonZero = ; char *pszChar = (char *)malloc(LEN_TEST_CHAR); //为简便起见未考虑内存分配错误处理
char *pszCharNext = (char *)malloc(LEN_TEST_CHAR); memset(pszChar, , LEN_TEST_CHAR);
strcpy(pszChar, szStrInited);
printf("pszChar: %p(%s), rVarInitNonZero:%d\n", pszChar, pszChar, rVarInitNonZero);
free(pszChar);
free(pszCharNext); return ;
}
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编程一站式学习》一书相应章节,在此致谢!
C语言变量的存储布局的更多相关文章
- c语言 变量的存储类别以及对应的内存分配?
<h4><strong>1.变量的存储类别</strong></h4>从变量值存在的角度来分,可以分为静态存储方式和动态存储方式.所谓静态存储方式指在程 ...
- [汇编与C语言关系]3. 变量的存储布局
以下面C程序为例: #include <stdio.h> ; ; ; int c; int main(void) { ; char b[] = "Hello World" ...
- C语言变量的存储类别
我们知道,从变量的作用域(即从空间)角度来分,可以分为全局变量和局部变量. 从另一个角度,从变量值存在的作时间(即生存期)角度来分,可以分为静态存储方式和动态存储方式. 静态存储方式:是指在程序运行期 ...
- C语言中变量的存储类型
在C语言中,对变量的存储类型说明有以下四种: auto 自动变量register 寄存器变量extern 外部变量static 静态变量 自动变量和寄存器变量属于动态存储方式,外部变量和静态变 ...
- C语言变量的类型和存储位置
. C语言变量主要分为全局变量.静态全局变量.局部变量.静态局部变量和寄存器变量.其中静态变量用static关键字进行修饰.程序所占用的内存可以分为以下几个部分: ()代码段-存放程序代码,只读的,不 ...
- C语言变量、函数的作用域及变量的存储方式
一.变量的作用域和存储方式 在C语言中每个变量都有两种基本属性:数据类型.数据的存储类别. 数据类型很多人都已熟知,例如:字符型(char).整型(int).浮点型(float)等等.存储类别是指数据 ...
- C语言学习笔记:12_变量的存储方式和生存期
/* * 12_变量的存储方式和生存期.c * * Created on: 2015年7月5日 * Author: zhong */ #include <stdio.h> #include ...
- Go 语言变量
变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念.变量可以通过变量名访问. Go 语言变量名由字母.数字.下划线组成,其中首个字母不能为数字. 声明变量的一般形式是使用 var 关键字: ...
- GO语言学习(七)Go 语言变量
Go 语言变量 变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念.变量可以通过变量名访问. Go 语言变量名由字母.数字.下划线组成,其中首个字母不能为数字. 声明变量的一般形式是使用 ...
随机推荐
- 开源轻量级分布式文件系统--FastDFS
FastDFS一个高效的分布式文件系统 分布式文件系统FastDFS原理介绍 分布式文件系统FastDFS设计原理 FastDFS安装.配置.部署(一)-安装和部署 分布式文件系统 - FastDFS ...
- Springboot学习笔记(七)-集成Redis
redis下载地址 添加依赖 <dependencies> <dependency> <groupId>org.springframework.boot</g ...
- 浅谈Linux系统中如何查看进程
进程是一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源.一般来说,Linux系统会在进程之间共享程序代码和系统函数库,所以在任何时刻内存中都只有代码的一份拷贝. 1,ps命令 作用:p ...
- 【玩转Golang】 自定义json序列化对象时,非法字符错误原因
由于前台web页面传来的日期对象是这样的格式“2010-11-03 15:23:22”,所以我安装网上查来的办法,自定义包装了time.Time对象,实现自己的Marshal和UnMarshal方法 ...
- 举例理解Hibernate的三种状态
初学Hibernate,了解到Hibernate有三种状态:transient(瞬时状态),persistent(持久化状态)以及detached(游离状态). 它们之间有如下转换图来说明: 1.tr ...
- js 或 且 非
给定 x=6 以及 y=3,下表解释了逻辑运算符: 运算符 描述 例子 && and (x < 10 && y > 1) 为 true || or (x== ...
- 安装office2010出现了错误,提示要安装MSXML6.10.1129.0解决方法
将下面的内容复制到记事本中,然后将记事本存成.reg文件 Windows Registry Editor Version 5.00[HKEY_CLASSES_ROOT\TypeLib\{F5078F1 ...
- ios的AutoresizingMask【转】
在 UIView 中有一个autoresizingMask的属性,它对应的是一个枚举的值(如下),属性的意思就是自动调整子控件与父控件中间的位置,宽高. enum { UIViewAutoresi ...
- Linux+Redis实战教程_day01_常用命令【重点】
3.常用命令[重点] Linux命令中参数,一般都是无序的.特殊情况下除外 3.1.磁盘管理命令 ls命令:列出目录内容 参数: -a 查询所有文件和文件夹.包含隐藏的 -l 查询详细列表 ls ...
- 诡异的DataTime.Now.ToString()
昨天晚上调程序的时候在服务器上出现这种问题 DataTime.Now.ToString("yyyy-MM-dd HH:mm:ss") 居然出现了2014-8-14 8:nn:14: ...