C++内存模型
〇、前言
本文将会讨论:Linux 下 C、C++的内存模型。
一、C
以下是一个示例,通过打印地址的值以及借助 nm
工具,来判断内存区域:
1 |
|
运行结果:
1 |
|
再看看符号地址:
1 |
|
先从地址最低的变量开始分析:
变量 | 类型 | 地址 | 区段 |
---|---|---|---|
G | global const int G = 5 | 0x55fd24d56008 | R |
H | global const char H = 6 | 0x55fd24d5600c | R |
Cptr | ‘’123456’’ | 0x55fd24d5600d | R |
C | global int C = 2 | 0x55fd24d58010 | D |
F | global static int F = 4 | 0x55fd24d58014 | d |
f | static int f = 4 | 0x55fd24d58018 | d |
A | global int A | 0x55fd24d58020 | B |
B | global int B = 0 | 0x55fd24d58024 | B |
D | global static int D | 0x55fd24d58028 | b |
E | global static int E = 0 | 0x55fd24d5802c | b |
d | static int d | 0x55fd24d58030 | b |
e | static int e = 0 | 0x55fd24d58034 | b |
变量分析
G (global const int G = 5)
- 类型:
R
(只读数据段) - 原因: 因为
G
是一个全局的const
变量,其值在编译时就已确定,且不可变,因此放在只读数据段。
- 类型:
H (global const char H = 6)
- 类型:
R
(只读数据段) - 原因: 与
G
相似,H
也是一个全局的const
变量,不可变。
- 类型:
Cptr (‘123456’)
- 类型:
R
(只读数据段) - 原因:
Cptr
指向的是一个字符串字面量,字符串字面量通常存储在只读数据段中,以防止被修改。
- 类型:
C (global int C = 2)
- 类型:
D
(已初始化的数据段) - 原因:
C
是一个已初始化的全局变量,存储在数据段中。
- 类型:
F (global static int F = 4)
- 类型:
d
(局部数据段,但这里应理解为私有初始化数据) - 原因:
F
是一个静态全局变量,已初始化,并且它是私有的,因此与全局非静态变量相比,存取权限有所不同。
- 类型:
f (static int f = 4)
- 类型:
d
(局部数据段,或私有初始化数据) - 原因: 类似于
F
,f
是静态的,已初始化,通常不会被程序的其他部分直接访问。
- 类型:
A (global int A)
- 类型:
B
(未初始化的全局数据段,bss) - 原因:
A
是一个全局未初始化的变量,默认为 0,放在 bss 段以节约空间。
- 类型:
B (global int B = 0)
- 类型:
B
(bss) - 原因: 虽然
B
被显式初始化为 0,但通常未执行任何操作的全局静态数据(即初始化为默认值)也会放在 bss 段。
- 类型:
D (global static int D)
- 类型:
b
(私有未初始化数据) - 原因:
D
是一个全局静态未初始化变量,通常存储在专门的静态数据段中,但私有。
- 类型:
E (global static int E = 0)
- 类型:
b
(私有未初始化数据) - 原因: 类似于
D
,尽管E
显式初始化为 0,但它是静态的且私有。
- 类型:
d (static int d)
- 类型:
b
(私有未初始化数据) - 原因:
d
作为静态变量,未初始化,私有存储。
- 类型:
e (static int e = 0)
- 类型:
b
(私有未初始化数据) - 原因: 尽管初始化为 0,
e
作为静态私有变量,其处理方式与其他未初始化静态变量相同。
- 类型:
至于局部变量:
1 |
|
可以看到,它们都位于栈区(地址很高):
变量 | 类型 | 地址 | 区段 |
---|---|---|---|
a | int a | 0x7ffe78385890 | stack |
b | int b = 0 | 0x7ffe78385894 | stack |
c | int c = 2 | 0x7ffe78385898 | stack |
g | const int g = 5 | 0x7ffe7838589c | stack |
二、C++
我们稍微对上一个程序进行修改,增加了一些变量:
1 |
|
运行结果:
1 |
|
打印一下符号表:
1 |
|
先从地址最低的变量开始分析:
变量 | 类型 | 地址 | 区段 |
---|---|---|---|
G | global const int G = 5 | 0x5651e330f008 | r |
H | global const char H = 6 | 0x5651e330f00c | r |
Cptr | ‘’123456’’ | 0x5651e330f00d | R |
C | global int C = 2 | 0x5651e3311010 | D |
Y | global long Y = 1 | 0x5651e3311018 | D |
F | global static int F = 4 | 0x5651e3311020 | d |
f | static int f = 4 | 0x5651e3311024 | d |
A | global int A | 0x5651e3311030 | B |
B | global int B = 0 | 0x5651e3311034 | B |
W | global long W | 0x5651e3311038 | B |
X | global long X = 0 | 0x5651e3311040 | B |
D | global static int D | 0x5651e3311048 | b |
E | global static int E = 0 | 0x5651e331104c | b |
d | static int d | 0x5651e3311050 | b |
e | static int e = 0 | 0x5651e3311054 | b |
可以看到,C/C++中,.data
和 .bss
的距离非常近。但是 C++对 .rodata 做出了 R、r 的区分。还可以看到内存对齐,比如:
变量 | 类型 | 地址 | 区段 |
---|---|---|---|
C | global int C = 2 | 0x5651e3311010 | D |
Y | global long Y = 1 | 0x5651e3311018 | D |
F | global static int F = 4 | 0x5651e3311020 | d |
C
的地址为 0x5651e3311010
,而 Y
的地址为 0x5651e3311018
。从 C
到 Y
的间隔是 8
个字节,而非 4
个字节。这表明编译器在 C
和 Y
之间插入了填充(padding),以确保 Y
能够在 8 字节边界上对齐。
三、总结
我们可以看到可执行程序内部都是分段进行存储的:
.text section
:代码段。通常存放已编译程序的机器代码,一般操作系统加载后,这部分是只读的。.rodatasection
:只读数据段。此段的数据不可修改,存放程序中会使用的常量。比如程序中的常量字符串 “aasdasdaaasdasd”。.datasection
:数据段。主要用于存放已初始化的不为 0 全局变量、静态变量。.bsssection
:bss
段。该段主要存储未初始化或者初始化为 0 的全局变量、未初始化以及初始化为 0 的全局静态变量、未初始化以及初始化为 0 的静态局部变量。
操作系统在加载 ELF 文件时会将按照标准依次读取每个段中的内容,并将其加载到内存中,同时为该进程分配栈空间,并将 pc
寄存器指向代码段的起始位置,然后启动进程。
从操作系统的本身来讲,以上存储区在该程序内存中的虚拟地址分布是如下形式(虚拟地址从低地址到高地址,实际的物理地址可能是随机的):.text→.data→.bss→heap→unused→stack→...
:
C++ 程序在运行时也会按照不同的功能划分不同的段,C++ 程序使用的内存分区一般包括:栈、堆、全局/静态存储区、常量存储区、代码区。
栈:目前绝大部分
CPU 体系都是基于栈来运行程序,栈中主要存放函数的局部变量、函数参数、返回地址等,栈空间一般由操作系统进行默认分配或者程序指定分配,栈空间在进程生存周期一直都存在,当进程退出时,操作系统才会对栈空间进行回收。
堆:动态申请的内存空间,就是由
malloc
函数或者new
函数分配的内存块,由程序控制它的分配和释放,可以在程序运行周期内随时进行申请和释放,如果进程结束后还没有释放,操作系统会自动回收。我们可以利用全局区/静态存储区:主要为
.bss
段和.data
段,存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 中,未初始化的放在.bss
段中,初始化的放在.data
段中,C++ 中不再区分了。常量存储区:
.rodata
段,存放的是常量,不允许修改,程序运行结束自动释放。代码区:
.text
段,存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。
参考:
作者:LeetCode
链接:https://leetcode.cn/leetbook/read/cmian-shi-tu-po/vv6a76/