语言基础(5):程序内存结构
1、C++进程内存分布
在C/C++程序进程内存分为:代码区、静态区、常量区、堆区、栈区,不同区域存放不同内容:
内存区 | 存放内容 | 说明 |
---|---|---|
代码区 | 二进制代码 | CPU可执行的机器指令,且是只读的 |
静态区 | 全局和静态变量 | 包括局部静态变量 |
常量区 | 程序在运行的期间不能够被改变的量 | 例如: 全局const修饰的变量,字符串常量”abcde”, 数组的名字等 |
堆区 | 存放malloc()函数申请的内存 | 忘记free()会造成内存泄露,程序结束由系统回收 new申请的内存可能在这个区域,取决于new的实现 |
栈区 | 存放局部变量 | 函数内的局部变量、形参和函数返回值,超出数据作用域,被系统回收 |
几点需要注意:
- 代码区,不仅存放程序二进制代码,还包括程序中使用的立即数,例如程序中直接出现的数值,int a = 10;// 10就是立即数;
- 静态区(static), 全局变量和静态变量存放区(RW),内存中,初始化的全局和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域(ZI:ZeroInit);
- 常量区,常变量和字符串常量、变量名等(RO:ReadOnly);
程序需要在计算机硬件上运行起来,其实质就是静态可执行的文件加载到内存中的过程,可执行程序文件只是一个程序的载体,程序运行最终要在内存中进行。
所谓动和静就是指内存分配方式,程序被加载到内存中:
- 静态分配内存:是在程序编译和链接时就确定好的内存;
- 动态分配内存:是在程序加载、调入、执行的时候分配/回收的内存;
int a = 0; // 静态区-初始化区
char *p1; // 静态区-未初始化区(ZI)
void main()
{
int b; // 栈区
char s[] = "abc"; // "abc"在常量区,s在栈区。
char *p2; // 栈
char *p3 = "123456"; // "123456\0", 在常量区,p3在栈区。
static int c =0; // 全局(静态)初始化区
/* p1,p2存放在栈区,存放 malloc 内存的首地址
* 分配得来得10和20字节的区域就在堆区。 */
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
strcpy(p1, "123456"); // "123456\0" 放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
#2、可执行文件结构 可执行文件,以windows为例,后缀格式为 .exe (文件结构是PE格式,linux可执行文件没有格式后缀,文件是ELF格式)文件,它可以加载到内存中,并由操作系统加载程序执行,是可在操作系统存储空间中浮动定位的可执行程序。
##2.1 可执行文件结构 通过dumpbin(win)、objdump(linux)可以查看可执行文件结构: - .text: 也称为代码段(Code),用来存放程序执行代码,同时也可能会包含一些常量(如一些字符串常量、立即数等),该段内存为静态分配,读写方式 RO(某些架构可能允许修改)。 这块内存是共享的,当有多个相同进程(Process)存在时,共用同一个 .text 段。 - .data: 也有的地方叫GVAR(global value),用来存放程序中已经初始化的非零全局变量。静态分配。.data又可分为读写(RW)区域和只读(RO)区域。 - RW段 则是普通非常全局变量,也包括静态变量; - RO段 保存常量所以也被称为 .constdata (linux中是独立的段 .rodata); - .bss 存放程序中为初始化的和零值全局变量,文件中并不分配存储空间,只记录变量空间大小。静态分配,在程序加载时通常会被清零(所以也叫ZI),读写方式 RW。 - stack: 栈,存放Automatic Variables,按内存地址由高到低方向生长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大,很明显该区域是 RW。 - heap: 堆,自由申请的空间,按内存地址由低到高方向生长,其大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大。(每个线程都会有自己的栈,但是堆空间是共用的),该区域RW。
2.2 可执行文件和进程对应关系
一个程序需要运行起来,除了映射加载到内存中,还需要更多的空间,完成数据间的交互,否则这个程序就是死的。所以我们还需要为更多的数据和数据交互提供一块内存 —— 堆栈。
-
栈(stack)
该段对应进程栈区,用于编译期间就能确定存储大小的存储区,主要用于在函数作用域内创建,离开作用域后自动销毁的变量。也即用于局部变量、函数参数等。栈的存储空间是连续的,两个紧挨着定义的局部变量,他们的存储空间也是紧挨着的。栈是向低地址扩展的数据结构,工作方式和数据结构中的栈类似。栈的大小是有限的,VC++编译器的默认栈的大小为1MB,所以不要定义int a[1000000]这样的超大数组,否则就会发生栈溢出(stack overflow)问题。 -
堆(heap)
该段进程堆区,用于编译期间不能确定存储大小的变量的存储区,运行期间确定,它的存储空间是不连续的,一般由malloc(可能 new)函数来分配内存块,并且需要用free(delete)函数释放内存。如果没有释放,就会出现内存泄漏(memory leak)问题。堆是向高地址扩展的数据结构,工作方式和数据结构中的链表类似,所以会产生内存碎片。堆的大小受OS虚拟内存大小限制,理论上每个程序最大可达4GB。 -
.data
该段对应进程分配存储的变量和常量- .data(RW)
映射进程全局/静态区,用于编译期间就能确定存储大小的变量的存储区,但它用于在整个程序运行期间都可见的全局变量和静态变量(全局的、局部的)。 - .data(RO)/.constdata
映射进程常量区,和“全局/静态存储区”一样,用于编译期间就能确定存储大小的常量的存储区,并且在程序运行期间,存储区内的常量是全局可见的,这是一块比较特殊的存储去,他们里面存放的是常量,不允许被修改。 - .bss
该段同样映射进程全局/静态区,主要对应未初始化全局变量,紧邻.data(RW)内存空间,在编译期间就能确定存储大小,但是可执行文件中不分配存储,程序加载时进程分配内存;
- .data(RW)
注意:
- 堆向高内存地址生长;
- 栈向低内存地址生长;
- 堆和栈相向而生,堆和栈之间有个临界点,称为stkbrk。
3、程序进程映射示例
3.1 进程内存映射实例
假设有如下一段程序
int main(...) {
fun_1(...);
fun_2(...);
fun_3(...);
return 0;
}
那么对应的内存映射结构如下所示:
3.2 函数帧栈
一个函数被调用时所建立的栈帧包含下面的信息:
- 被调函数的返回地址。返回地址是存放在被调用函数的栈帧里,还是主调函数的栈帧,取决于不同系统的实现;
- 主调函数的栈帧信息, 即栈顶和栈底;
- 为被调用函数的参数分配的空间(win 由右至左入栈,取决于不同系统的实现);
- 为函数的局部变量分配的栈空间。
3.3 linux 对象文件结构实例(和win类似)
#reference [C/C++程序内存的各种变量存储区域和各个区域详解](https://blog.csdn.net/jirryzhang/article/details/79518408) [操作系统执行可执行程序时,内存分配是怎样的](http://nvzhuang.ping-jia.net/jieshao-758155935643838964.htm) [详细的可执行程序结构参考](https://blog.csdn.net/edonlii/article/details/8779075) [C/C++内存分配](https://www.cnblogs.com/yongdaimi/p/6495182.html) [heap 和 free store区别](https://www.cnblogs.com/QG-whz/p/5060894.html)