【转】C/C++ 变量与内存(上篇)

引言

本文以问题的形式,通过真实的编译调试分析 C/C++ 中各类变量在编译、装载和运行时的特点,着重介绍各类变量运行时在内存中的位置。目的是以另一种角度,更深入地理解变量由代码编译为可执行文件,然后装载执行的原理。

⚠️ 注意

本文分析到可执行文件中 __bss 段、__data 段等层面,需要一定的计算机基础。

开发环境

  • OS X El Captian (10.11.6)
  • Xcode 7.3.1
  • Apple LLVM 7.3.0 (clang-703.0.31)

1、 全局变量和静态变量分别在内存哪个区域?已初始化和未初始化有区别吗?

实验

#include 

int global_init_a = 1;
char global_init_b = 'a';

int global_unin_a;
char global_unin_b;

int main(int argc, const char *argv[]) {
  static int local_stat_init_a = 2;
  static char local_stat_init_b = 'b';

  static int local_stat_unin_a;
  static char local_stat_unin_b;

  sleep(-1);
  return 0;
}

这里测试 2 个变量:int 型的 a 和 char 型的 b;测试 3 种类型的变量:全局变量(global)、局部静态变量(local_stat)以及局部变量(local);其中每类变量都有两种版本:已初始化(init)和未初始化(unin)。

先看看相关的段信息(objdump -x

Section
  sectname __text
   segname __TEXT
      addr 0x0000000100000f50
      size 0x000000000000003a
    offset 3920
     align 2^4 (16)
    reloff 0
    nreloc 0
      type S_REGULAR
Section
  sectname __data
   segname __DATA
      addr 0x0000000100001018
      size 0x000000000000000d
    offset 4120
     align 2^2 (4)
    reloff 0
    nreloc 0
      type S_REGULAR
Section
  sectname __bss
   segname __DATA
      addr 0x0000000100001028
      size 0x0000000000000005
    offset 0
     align 2^2 (4)
    reloff 0
    nreloc 0
      type S_ZEROFILL
Section
  sectname __common
   segname __DATA
      addr 0x0000000100001030
      size 0x0000000000000005
    offset 0
     align 2^2 (4)
    reloff 0
    nreloc 0
      type S_ZEROFILL

再看看符号表 ( objdump -t

0000000100001018 (__DATA,__data) external _global_init_a
000000010000101c (__DATA,__data) external _global_init_b
0000000100001020 (__DATA,__data) non-external _main.local_stat_init_a
0000000100001024 (__DATA,__data) non-external _main.local_stat_init_b
0000000100001028 (__DATA,__bss) non-external _main.local_stat_unin_a
000000010000102c (__DATA,__bss) non-external _main.local_stat_unin_b
0000000100001030 (__DATA,__common) external _global_unin_a
0000000100001034 (__DATA,__common) external _global_unin_b

这里明显可以看出,已初始化的全局变量和局部静态变量都在 __data 段中,而未初始化的全局变量在 __common 段中,未初始化的局部静态变量在 __bss 段中。

结论

已初始化的全局变量和局部静态变量都在 __data 段中,而未初始化的全局变量 在__common 段中,未初始化的局部静态变量在 __bss 段中。

2、全局变量和静态变量初始化为 0 是不是就 在 __data 段中了?

这个比较简单,就不做实验验证了,初始化为 0 的全局变量和静态变量还是按照未初始化处理的,即该在 __bss 段还在 __bss 段,该在 __common 段还在 __common 段。(容易理解,对于全局变量和静态变量来说,初始化为 0 和未初始化是一模一样的,因为按照未初始化来效果会更好,所以就当做是未初始化了。)

3、__common段和 __bss 段有什么区别?

简介

在【问1】中我们看到未初始化的全局变量在 __common 段中,未初始化的局部静态变量在 __bss 段中;__common 段并不是一个常见的段,为什么不都放在 __bss 段中呢?

分析

实际上使用不同的语言或不同的编译器,得到的结果都不太一样,比如 GCC 编译不会有 __common 段存在,未初始化的全局变量仅存在于符号表中。

仔细观察上图中的段表可以发现,__common 段紧跟着 __bss 段,且相关属性也大体相同。再看符号表也能看到类似的结果:作为 __common 段首的 global_unin_a 也是紧跟在 __bss 段尾的 ocal_stat_unin_b 之后的。

结论

虽然这里没有非常明显的证据,但我们还是可以认为 __common 段和 __bss 段没有太大区别;而之所以 __common 段独立于 __bss 段,是因为要考虑到 全局变量需要暴露给外部(external) ,涉及到“弱符号与强符号”的问题(这里不作介绍),否则与 __bss 段没区别。

4、如何理解 “__bss 段不占用可执行文件空间”?

简介

相信在很多介绍 __bss 段的文章中都提到说 “__bss 段不占用可执行文件空间”,意思是说 __bss 段 在可执行文件中是不实际存在的。但在 __bss 段信息中又是有长度的,这个该如何理解呢?

实验

这里首先介绍一些基础知识,关于 ELF 文件(Linux下可执行文件等都是 ELF 文件)的结构,如下表:

ELF Header
Symbol Tables
String Tables
Section header table
.text
.data
.bss
...
...

【问1】中列出的段信息实际是在读取段表(Section Header Table),段表描述了每个段的段名、长度、偏移、读写权限等信息;并且[问1]中列出的符号表也是直接读取的 符号表(Symbol Table),符号表中就记录了全局变量和局部静态变量的地址、占用空间大小等信息(符号表包含的信息远不止这些)。

既然段信息和变量信息都在专门的区域保存着,那 .data 和 .bss 中还剩下什么呢?不妨直接用命令看看。
__data 段内容:

Contents of (__DATA,__data) section
0000000100001018           01 00 00 00 61 00 00 00 02 00 00 00 62

结合符号表很容易看出来,内存位置 0000000100001018 是 global_init_a,其值为 01 00 00 00,就是 1 了;紧随其后的是 global_init_b,值为 61,就是 'a';再后面是 local_stat_init_a,值为 02 00 00 00,即 2;最后是 local_stat_init_b,值为 62,即 'b'。意思就是 __data 段保存的是已初始化的全局变量和局部静态变量的值。

下面看看 __bss 段内容:

Contents of (__DATA,__bss) section
zerofill section and has no contents in the file

__bss 段在可执行文件中没有任何内容,这个其实不难理解。存放在 __bss 段的是未初始化的全局变量和局部静态变量,既然没有初始化,可执行文件中也就不需要专门去记录变量的值了(也没有值拿来记录),唯一需要的就是给这些变量一个确定的内存地址(像 __data 段中的变量一样)。这样其实有两种方法:其一,像 __data 段那样在相应位置写一些初始值进去占位,可执行文件装载时直接映射就好了,和 __data 段一模一样;其二,不给 __bss 段在可执行文件中占位,在装载时根据 __bss 段信息直接在内存中开辟相应区域,即将占位从可执行文件推迟到装载时。 编译器就是选择的方法二,将占位从可执行文件推迟到装载时,这样做的好处就是减小了可执行文件的体积,比如一个长度为 10000 的未初始化 int 数组,采用方法二不会占用任何可执行文件空间,而采用方法一则会将可执行文件增大至少 40KB。

如果你对此还有疑问,不妨在程序运行时暂停看看相关变量的内存信息,如下:

global_init_a 内存地址为 0x100001018

100001018   01 00 00 00 61 00 00 00 02 00 00 00 62

local_stat_unin_a 内存地址为 0x100001028

100001028   00 00 00 00 00 00 00 00 00 00 00 00 00

运行时内存中为 __bss 段中变量分配了内存空间。

结论

__bss 段确实不占用可执行文件空间,但文件装载后在内存中还是会占用相应大小的空间的,这种处理方法就是为了减少可执行文件大小,避免不必要的开销。

但严格地说,__bss 段也还是占用一些可执行文件空间的,比如在段表中有 __bss 段的描述,在符号表中有 __bss 段内相关变量的描述,但这里就不是同一个概念了。

5、__data 段或 __bss段的大小就是其内部变量大小之和吗?

简介

既然 __data 段的内容就是其包含的变量的值,那么是不是 __data 段占用的内容空间就是其包含的各变量占用内存空间的和呢?

分析

上图的例子中,__data 段中包含 4 个变量(global_init_a、global_init_b、local_stat_init_a 和 local_stat_init_b),其中 2 个 int、2 个 char。这么计算 __data 段应该一共占用 10 byte,但段信息却显示 __data 段一共占用了 13 byte,多出了 3 byte。

如果仔细看【问3】的话,其实已经看出端倪了,这里再把 __data 段中变量的内存地址和值写的明显一点:

global_init_a       0x100001018
global_init_b       0x10000101c
local_stat_init_a   0x100001020
local_stat_init_b   0x100001024
100001018   01 00 00 00 61 00 00 00
100001020   02 00 00 00 62 00 00 00

global_init_b 和 local_stat_init_a 之间间隔了 3 byte,这是内存中常见的“对齐”处理。

这个对齐是从哪来的呢?不妨看看汇编代码中 __data 段部分:

.section    __DATA,__data
.globl    _global_init_a          ## @global_init_a
.align    2
_global_init_a:
.long    1                        ## 0x1

.globl    _global_init_b          ## @global_init_b
_global_init_b:
.byte    97                       ## 0x61

.align    2                       ## @main.local_stat_init_a
_main.local_stat_init_a:
.long    2                        ## 0x2

_main.local_stat_init_b:          ## @main.local_stat_init_b
.byte    98                       ## 0x62

__dat a段中有两个 .align 2,意思是内存地址与 2 的 2 次幂(即 4)对齐,简单来说就是内存指针往后移动到第一个地址能被 4 整除的地址。第一个 .align 2 就是 __data 段的段首,进行对齐(__data 段信息也有类似的描述),仔细想想是不是 __data 段前面也有可能有几个 byte 没有用只拿来对齐,而又没有算到 __data 段中?第二个 .align 2 就跳过了 3 个 byte 对齐到 0x100001020。

这里就不仔细介绍内存对齐的原因了,那是计算机组成原理的范畴。

结论

__data 段或 __bss 段的大小不一定是其内部变量大小之和,一般会大于或等于其内部变量大小之和。这是内存对齐造成的。

6、局部变量在内存哪个区域?已初始化和未初始化有区别吗?

简介

上面介绍了全局变量和局部静态变量的相关内容,我们不禁好奇一般的局部变量存储在内存的哪个区域呢?是不是也在哪个段中?

实验

我们在【问1】代码的基础上,加上一般变量:

#include 

int global_init_a = 1;
char global_init_b = 'a';

int global_unin_a;
char global_unin_b;

int main(int argc, const char *argv[]) {
  static int local_stat_init_a = 2;
  static char local_stat_init_b = 'b';

  static int local_stat_unin_a;
  static char local_stat_unin_b;

  int local_init_a = 3;
  char local_init_b = 'c';

  int local_unin_a;
  char local_unin_b;

  sleep(-1);
  return 0;
}

这次我们来看看 main() 的反汇编代码:

Test_C`main:
    0x100000f50 <+0>:  pushq  %rbp
    0x100000f51 <+1>:  movq   %rsp, %rbp
    0x100000f54 <+4>:  subq   $0x30, %rsp
    0x100000f58 <+8>:  movl   $0xffffffff, %eax         ; imm = 0xFFFFFFFF
    0x100000f5d <+13>: movl   $0x0, -0x4(%rbp)
    0x100000f64 <+20>: movl   %edi, -0x8(%rbp)
    0x100000f67 <+23>: movq   %rsi, -0x10(%rbp)
    0x100000f6b <+27>: movl   $0x3, -0x14(%rbp)
    0x100000f72 <+34>: movb   $0x63, -0x15(%rbp)
    0x100000f76 <+38>: movl   %eax, %edi
    0x100000f78 <+40>: callq  0x100000f8a               ; symbol stub for: sleep
->  0x100000f7d <+45>: xorl   %edi, %edi
    0x100000f7f <+47>: movl   %eax, -0x24(%rbp)
    0x100000f82 <+50>: movl   %edi, %eax
    0x100000f84 <+52>: addq   $0x30, %rsp
    0x100000f88 <+56>: popq   %rbp
    0x100000f89 <+57>: retq   

这里 rbp 就是帧指针(Frame Pointer,指向函数活动记录的一个固定位置),而 rsp 就是指向栈顶的指针了(这里是函数调用的基础知识,如有疑问还请查阅资料)。我们主要关注这两行

0x100000f6b <+27>: movl   $0x3, -0x14(%rbp)
0x100000f72 <+34>: movb   $0x63, -0x15(%rbp)

其对应代码中的:

int local_init_a = 3;
char local_init_b = 'c';

我们知道栈是往小内存地址生长的,根据反汇编代码不难看出,局部变量存储在栈中。

我们不妨在运行时看看相关变量的内存地址和值:

local_init_a    0x7fff5fbff93c
local_init_b    0x7fff5fbff93b
local_unin_a    0x7fff5fbff934
local_unin_b    0x7fff5fbff933
7fff5fbff933    00 00 00 00 00 00 00 00
7fff5fbff93b    63 03 00 00 00 70 F9 BF

也可以看出这些局部变量都按顺序存储在栈中。

结论

局部变量存储在栈中,已初始化和未初始化没有区别。

7、全局变量、静态变量和局部变量的默认值都是 0 吗?

简介

未初始化的全局变量、静态变量和局部变量在程序运行时都会占用内存空间,也就是说都会有一个默认值(就是所在位置内存的值),那么这些默认值都会是 0 吗?

这个问题的答案在刚开始学 C 语言的时候就知道了:全局变量和静态变量的默认值是 0,局部变量的默认值不确定。这里就来仔细分析一下为什么会是这样。

分析

全局变量、静态变量

全局变量和局部静态变量都是存储在 __bss 段和 __common 段的,关于默认值只需要以段为单位分析就好了,这里以 __bss 段为例。

再看看 __bss 段信息:

ection
  sectname __bss
   segname __DATA
      addr 0x0000000100001028
      size 0x0000000000000005
    offset 0
     align 2^2 (4)
    reloff 0
    nreloc 0
      type S_ZEROFILL

注意到 type S_ZEROFILL,就是要求此段内容全部填充 0.
作为佐证,再看看汇编代码 __bss 段部分:

.zerofill __DATA,__bss,_main.local_stat_unin_a,4,2 ## @main.local_stat_unin_a
.zerofill __DATA,__bss,_main.local_stat_unin_b,1,0 ## @main.local_stat_unin_b

.zerofill 指令就是对指定内存地址,指定长度填充 0。

这里再来理一遍:可执行文件在装载时,很据段表得到 __bss 段内存起始位置和大小,为其分配空间后将此内存空间全部填充 0。程序运行时相应变量的内存值就是全 0,在 C 语言中就是对应各种变量类型的默认值。

局部变量

在【问5】的反汇编代码中并不存在对未初始化局部变量填充 0 的代码,而仅仅是为其分配了空间,所以局部变量的默认值是不确定的。(但查看内存还是发现这些未初始化的局部变量的值都是默认值,还不太清楚是什么时候填充 0 的)

原因分析

为什么全局变量和静态变量默认值是 0,而局部变量不确定?其实很容易理解。全局变量和静态变量存储在 __bss 段和 __common 段中,装载时内存地址已知,填充 0 开销不太大,且全局变量和静态变量作用于整个程序生命周期,对其进行初始化也是有价值的。反观局部变量,局部变量数量众多,生命周期短,存储在栈中且内存地址不能事先确定,如果每次都对局部变量填充 0 初始化,不仅消耗资源,且收益较小,得不偿失。

结论

全局变量和静态变量的默认值是 0,局部变量的默认值不确定

8、const 修饰的变量在内存中位置会有不同吗?

简介

const 修饰的变量会变成常量,其值不能再更改,那这些变量会放在哪里呢?

实验

在【问5】的代码基础上,我们为每个变量都增加一个 const 版:

#include 

int global_init_a = 1;
char global_init_b = 'a';

const int global_con_init_a = 2;
const char global_con_init_b = 'b';

int global_unin_a;
char global_unin_b;

const int global_con_unin_a;
const char global_con_unin_b;

int main(int argc, const char *argv[]) {
  static int local_stat_init_a = 3;
  static char local_stat_init_b = 'c';

  const static int local_con_stat_init_a = 4;
  const static char local_con_stat_init_b = 'd';

  static int local_stat_unin_a;
  static char local_stat_unin_b;

  const static int local_con_stat_unin_a;
  const static char local_con_stat_unin_b;

  int local_init_a = 5;
  char local_init_b = 'e';

  const int local_con_init_a = 6;
  const char local_con_init_b = 'f';

  int local_unin_a;
  char local_unin_b;

  const int local_con_unin_a;
  const char local_con_unin_b;

  sleep(-1);
  return 0;
}

全局变量、静态变量

看看相关的段信息:

Section
  sectname __text
   segname __TEXT
      addr 0x0000000100000f30
      size 0x0000000000000045
    offset 3888
     align 2^4 (16)
    reloff 0
    nreloc 0
      type S_REGULAR
Section
  sectname __const
   segname __TEXT
      addr 0x0000000100000f98
      size 0x000000000000001d
    offset 3992
     align 2^2 (4)
    reloff 0
    nreloc 0
      type S_REGULAR
Section
  sectname __data
   segname __DATA
      addr 0x0000000100001018
      size 0x000000000000000d
    offset 4120
     align 2^2 (4)
    reloff 0
    nreloc 0
      type S_REGULAR
Section
  sectname __bss
   segname __DATA
      addr 0x0000000100001028
      size 0x0000000000000005
    offset 0
     align 2^2 (4)
    reloff 0
    nreloc 0
      type S_ZEROFILL
Section
  sectname __common
   segname __DATA
      addr 0x0000000100001030
      size 0x0000000000000005
    offset 0
     align 2^2 (4)
    reloff 0
    nreloc 0
      type S_ZEROFILL

再看看相关的符号表:

0000000100000f98 (__TEXT,__const) external _global_con_init_a
0000000100000f9c (__TEXT,__const) external _global_con_init_b
0000000100000fa0 (__TEXT,__const) non-external _main.local_con_stat_init_a
0000000100000fa4 (__TEXT,__const) non-external _main.local_con_stat_init_b
0000000100000fa8 (__TEXT,__const) non-external _main.local_con_stat_unin_a
0000000100000fac (__TEXT,__const) non-external _main.local_con_stat_unin_b
0000000100000fb0 (__TEXT,__const) external _global_con_unin_a
0000000100000fb4 (__TEXT,__const) external _global_con_unin_b
0000000100001018 (__DATA,__data) external _global_init_a
000000010000101c (__DATA,__data) external _global_init_b
0000000100001020 (__DATA,__data) non-external _main.local_stat_init_a
0000000100001024 (__DATA,__data) non-external _main.local_stat_init_b
0000000100001028 (__DATA,__bss) non-external _main.local_stat_unin_a
000000010000102c (__DATA,__bss) non-external _main.local_stat_unin_b
0000000100001030 (__DATA,__common) external _global_unin_a
0000000100001034 (__DATA,__common) external _global_unin_b

最后看看 __const 段的内容:

Contents of (__TEXT,__const) section
0000000100000f98           02 00 00 00 62 00 00 00 04 00 00 00 64 00 00 00
0000000100000fa8           00 00 00 00 00 00 00 00 00 00 00 00 00

可以发现 所有以 const 修饰的全局变量和局部静态变量都放在了只读的 __const 段中。与不加 const 修饰的全局变量和局部静态变量相比,主要有以下几个区别:

  • 暴露在外(external)的全局变量和内部(non-external)的局部静态变量都在 __const 段中;
  • 已初始化和未初始化的变量都在 __const 段中;
  • __const 段没有如 __bss 段节约可执行文件空间的特性。

也就是说,未初始化的全局变量和局部静态变量都会占用可执行文件空间。

局部变量

我们还是照【问5】来判断局部变量所在的位置。

反汇编:

Test_C`main:
    0x100000f30 <+0>:  pushq  %rbp
    0x100000f31 <+1>:  movq   %rsp, %rbp
    0x100000f34 <+4>:  subq   $0x40, %rsp
    0x100000f38 <+8>:  movl   $0xffffffff, %eax         ; imm = 0xFFFFFFFF
    0x100000f3d <+13>: movl   $0x0, -0x4(%rbp)
    0x100000f44 <+20>: movl   %edi, -0x8(%rbp)
    0x100000f47 <+23>: movq   %rsi, -0x10(%rbp)
    0x100000f4b <+27>: movl   $0x5, -0x14(%rbp)
    0x100000f52 <+34>: movb   $0x65, -0x15(%rbp)
    0x100000f56 <+38>: movl   $0x6, -0x1c(%rbp)
    0x100000f5d <+45>: movb   $0x66, -0x1d(%rbp)
    0x100000f61 <+49>: movl   %eax, %edi
    0x100000f63 <+51>: callq  0x100000f76               ; symbol stub for: sleep
->  0x100000f68 <+56>: xorl   %edi, %edi
    0x100000f6a <+58>: movl   %eax, -0x34(%rbp)
    0x100000f6d <+61>: movl   %edi, %eax
    0x100000f6f <+63>: addq   $0x40, %rsp
    0x100000f73 <+67>: popq   %rbp
    0x100000f74 <+68>: retq   

注意无 const 修饰和有 const 修饰的这4行:

0x100000f4b <+27>: movl   $0x5, -0x14(%rbp)
0x100000f52 <+34>: movb   $0x65, -0x15(%rbp)
0x100000f56 <+38>: movl   $0x6, -0x1c(%rbp)
0x100000f5d <+45>: movb   $0x66, -0x1d(%rbp)

说明对于已初始化局部变量,有无 const 修饰对变量所在的位置没有影响,都是按顺序在栈中。

看看相关变量的内存地址和值:

local_init_a      0x7fff5fbff93c
local_init_b      0x7fff5fbff93b
local_con_init_a  0x7fff5fbff934
local_con_init_b  0x7fff5fbff933
local_unin_a      0x7fff5fbff92c
local_unin_b      0x7fff5fbff92b
local_con_unin_a  0x7fff5fbff924
local_con_unin_b  0x7fff5fbff923
7fff5fbff923    00 00 00 00 00 00 00 00
7fff5fbff92b    00 00 00 00 00 00 00 00
7fff5fbff933    66 06 00 00 00 00 00 00
7fff5fbff93b    65 05 00 00 00 70 F9 BF

也印证了,对于局部变量,有无 const 修饰对变量所在的位置没有影响,都是按顺序在栈中。

结论

由 const 修饰的全局变量和局部静态变量,会放在只读的 __const 段中;而对于局部变量,是否有 const 修饰对变量的位置没有影响,都是在栈中。
注意这里有个问题:__const 段只读是由操作系统保护,其内部的值不能修改;但有 const 修饰的局部变量没有保护机制(因为和一般局部变量一样放在栈中),这时 C 语言想要实现 const 的功能,只能干预编译过程,实现常量化。这个问题留到以后去考虑。

9、 字符串存放在哪里呢?

字符串的情况比较特殊,将另作一篇写,请参考:【C/C++ 变量与内存(下篇)】

10、那哪些变量是放在堆中的呢?

在堆中的情况很简单,这里就不用实验分析了。

在 C/C++ 中只要是使用到 malloc() 申请到的内存空间,全都在堆中。

那么 C++ 中 new 得到的变量呢?当然也在堆里了,看看 new 的源码:

operator new (std::size_t sz, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
{
  void *p;

  /* malloc (0) is unpredictable; avoid it.  */
  if (sz == 0)
    sz = 1;

  while (__builtin_expect ((p = malloc (sz)) == 0, false))
    {
        new_handler handler = std::get_new_handler ();
        if (! handler)
          return 0;
        __try
          {
            handler ();
          }
         __catch(const bad_alloc&)
          {
            return 0;
          }
    }

  return p;
}

new 实际就是用的 malloc() 申请堆空间,当然是在堆中了。


参考

  • 【原文链接】

你可能感兴趣的:(【转】C/C++ 变量与内存(上篇))