前言

本文以问题的形式,通过真实的编译调试分析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. 全局变量和静态变量分别在内存哪个区域?已初始化和未初始化有区别吗?

简介

相信对编译有些了解的都能回答出一二,这里还是用实验去探寻一番。

实验

C代码:

#include <unistd.h>

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型的achar型的b;测试3种类型的变量:全局变量(global)、局部静态变量(local_stat)以及局部变量(local);其中每类变量都有两种版本:已初始化(init)和未初始化(unin)。

先看看相关的段信息:

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

再看看符号表:

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段存在,未初始化的全局变量仅存在于符号表中。

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

结论

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

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

简介

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

实验

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

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

[问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

结合符号表很容易看出来,内存位置0000000100001018global_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段占用的内容空间就是其包含的各变量占用内存空间的和呢?

分析

我们继续以[问1]中的程序为例子,__data段中包含4个变量(global_init_aglobal_init_blocal_stat_init_alocal_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_blocal_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

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

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

结论

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

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

简介

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

实验

我们在[问1]中C代码的基础上,加上一般变量:

#include <unistd.h>

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)

其对应C代码中的

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段信息:

Section
  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 <unistd.h>

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. 字符串存放在哪里呢?

字符串的情况比较特殊,将另作一篇写,敬请期待!

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()申请堆空间,当然是在堆中了。

一点唠叨

虽然本文分析采用的Clang编译器,而不是常用的gcc,但正所谓万变不离其宗,编译器五花八门,但程序运行的原理还是一样的。

本文写作仓促,还望不吝赐教。

参考