程序中的数据存储剖析

未初始化的全局变量(.bss段)

  已经记不清bss代表Block Storage Start还是Block Started by Symbol。像我这种没有和那些古董级计算机打过交道的人,终究无法理解这样怪异的名字,记不住也就不足为奇了。不过没有关系,我们不必纠结于bss究竟代表什么,而是要弄清楚bss段中都会存放些什么数据、这些数据都有什么样的特点以及我们该如何使用它们。

  通俗地讲,bss段被用来存放那些没有初始化或初始化为0的全局变量。它有什么特点呢,让我们先来看看一个小程序的表现。

  int bss_array[1024 * 1024];

  int main(int argc, char* argv[])

  {

  return 0;

  }

  # gcc -g bss.c -o bss.exe

  # ls -l bss.exe

  -rwxrwxr-x 1 root root 5975 Nov 16 09:32 bss.exe

  # objdump -h bss.exe

  grep bss

  24 .bss 00400020 080495e0 080495e0 000005e0 2**5

  变量bss_array的大小为4M,而可执行文件的大小只有5K。由此可见,bss类型的全局变量只占运行时的内存空间,而不占用文件空间。

  现在大多数操作系统在加载程序时,会把所有的bss全局变量清零。但为了保证程序的可移植性,最好能手工把这些变量初始化为0,这样可以使这些变量都有个确定的初始值。

  当然了,作为全局变量,在整个程序的运行周期内,bss数据是一直存在的。

  初始化过的全局变量(.data段)

  与bss相比,data段就容易理解多了,看名称就大概能知道它里面存放着数据。当然,如果数据全是0,为了优化考虑,编译器会把它当作bss处理。通俗地讲,data段被用来存放那些初始化为非0值的全局变量。那么它又有什么特点呢,我们还是先来看看一个小程序的表现。

  int data_array[1024 * 1024] = {1};

  int main(int argc, char* argv[])

  {

  return 0;

  }

  # ls -l data.exe

  -rwxrwxr-x 1 root root 4200313 Nov 16 09:34 data.exe

  # objdump -h data.exe

  grep //.data

  23 .data 00400020 080495e0 080495e0 000005e0 2**5

  仅仅是把初始化的值改为非0值了,文件就变为4M多。由此可见,data类型的全局变量是既占文件空间,又占用运行时内存空间的。

  同样,作为全局变量,在整个程序的运行周期内,data数据也是一直存在的。

  常量数据(.rodata段)

  rodata的意义同样明显,ro代表read only(只读),rodata就是用来存放常量数据的。关于rodata类型的数据,要注意以下几点。
(1) 常量不一定就放在rodata里,有的立即数直接和指令编码在一起,存放在代码段(.text)中。

  (2) 对于字符串常量,编译器会自动去掉重复的字符串,保证一个字符串在一个可执行文件(EXE/SO)中只存在一个副本。

  (3) rodata是在多个进程间共享的,这样可以提高运行空间利用率。

  (4) 在有的嵌入式系统中,rodata放在ROM(或者NOR闪存芯片)里,运行时直接读取,无需加载到RAM中。

  (5) 在嵌入式Linux系统中,也可以通过一种叫作XIP(就地执行)的技术直接读取常量数据,而无需加载到RAM中。

  (6) 常量是不能修改的,修改常量在Linux下会出现段错误。

  由此可见,把在运行过程中不会改变的数据设为rodata类型是有好处的。在多个进程间共享,可以大大提高空间利用率,甚至能不占用RAM空间。同时由于rodata在只读的内存页面中是受保护的,任何试图对它进行修改的行为都会被及时发现,这样一来还可以提高程序的稳定性。

  字符串会被编译器自动放到rodata中,其他数据要放到rodata中,只需要为其加const关键字修饰即可。

  代码(.text段)

  text段存放代码(如函数)和部分整数常量,它与rodata段很相似,相同的特性我们就不重复了,主要的区别在于text段是可以执行的。

  栈(stack)

  栈是用来存放临时变量和函数参数的。将栈作为一种基本数据结构,我并不感到惊讶;将其用来实现函数调用,也是大家司空见惯的作法。直到我试图找到另外一种方式实现递归操作时,我才感叹于栈的巧妙。要实现递归操作,不用栈不是不可能,只是找不出比使用栈更优雅的方式。

  尽管大多数编译器在优化时会把常用的参数或局部变量放入寄存器中,但用栈来管理函数调用时的临时变量(局部变量和参数)才是通行的做法,前者只是辅助手段而已,而且只可以在当前函数中将参数和局部变量存入寄存器,一旦调用下一层函数,这些值还是得存入栈中才行。

  通常情况下,栈是向下(低地址)增长的,每向栈中PUSH一个元素,栈顶就向低地址扩展,每从栈中POP一个元素,栈顶就向高地址回退。这里有一些比较有意思的问题:在x86平台上,栈顶寄存器为ESP,那么ESP的值是在PUSH操作之前修改呢,还是在PUSH操作之后修改呢?PUSH ESP这条指令会向栈中存入什么数据呢?据说x86系列CPU中,除了286外,都是先修改ESP,再压栈的。由于286没有CPUID指令,因此有的操作系统会用这种方法检查286的型号。

  要注意的是,存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了,继续访问这些变量会造成意想不到的错误。
堆(heap)

  堆是最灵活的一种内存,它的生命周期完全由使用者控制。标准C提供以下几个函数来使用堆内存。

  函数名 用途

  Malloc 用来分配一块指定大小的内存。

  Realloc 用来调整/重分配一块存在的内存。

  free 用来释放不再使用的内存。

  使用堆内存时请注意以下两个问题。

  1, alloc/free要配对使用。我们将内存分配了而不释放的情形称为内存泄露(memory leak),如果内存泄露的情况过多出现,迟早会造成“Out of memory”(内存不足)的错误,从而无法再成功分配内存。当然释放时也只能释放已经被分配的内存,释放无效的内存或重复释放都是不行的,会造成程序崩溃。

  2, 分配多少用多少。分配了100字节就只能用100字节,不管是读还是写,都只能在这个范围内,读多了会读到随机的数据,写多了会造成随机的破坏。我们将读写分配范围外的数据的情况称为缓冲区溢出(buffer overflow),这种情况是非常严重的,大部分安全问题都是由缓冲区溢出引起的。

  想手工检查有无内存泄露或缓冲区溢出是很困难的,幸好有些工具可供使用,比如Linux下有valgrind,它的使用方法很简单,大家下去可以试用一下,以后每次写完程序都应该用valgrind跑一遍。

  最后,我们来看看在Linux下,程序运行时空间的分配情况。

  # cat /proc/self/maps

  00110000-00111000 r-xp 00110000 00:00 0 [vdso]

  009ba000-009d6000 r-xp 00000000 08:01 768759 /lib/ld-2.8.so

  009d6000-009d7000 r--p 0001c000 08:01 768759 /lib/ld-2.8.so

  009d7000-009d8000 rw-p 0001d000 08:01 768759 /lib/ld-2.8.so

  009da000-00b3d000 r-xp 00000000 08:01 768760 /lib/libc-2.8.so

  00b3d000-00b3f000 r--p 00163000 08:01 768760 /lib/libc-2.8.so

  00b3f000-00b40000 rw-p 00165000 08:01 768760 /lib/libc-2.8.so

  00b40000-00b43000 rw-p 00b40000 00:00 0

  08048000-08050000 r-xp 00000000 08:01 993652 /bin/cat

  08050000-08051000 rw-p 00007000 08:01 993652 /bin/cat

  0805f000-08080000 rw-p 0805f000 00:00 0 [heap]

  b7fe8000-b7fea000 rw-p b7fe8000 00:00 0

  bfee7000-bfefc000 rw-p bffeb000 00:00 0 [stack]

  每个区间都有四个属性:

  r 表示可以读取;

  w 表示可以修改;

  x 表示可以执行;

  p/s 表示是否为共享内存。

  对有文件名的内存区间而言:

  属性为r--p表示存放的是rodata;

  属性为rw-p表示存放的是bss和data;

  属性为r-xp表示存放的是text数据。

  没有文件名的内存区间则表示用mmap映射的匿名空间。

  文件名为[stack]的内存区间表示是栈。

  文件名为[heap]的内存区间表示是堆。

  对内存的掌握是系统程序员必备的技能,希望大家多加体会。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值