C++程序内存布局
来源:互联网 发布:早期复极综合征知乎 编辑:程序博客网 时间:2024/05/29 14:09
转帖地址:
http://blog.csdn.net/imyfriend/article/details/8497103
对任何一个普通C++程序来讲,它都会涉及到5种不同的数据段。常用的几个数据段种包含有“程序代码段”、“程序数据段”、“程序堆栈段”等。不错,这几种数据段都在其中,但除了以上几种数据段之外,进程还另外包含两种数据段。下面我们来简单归纳一下进程对应的内存空间中所包含的5种不同的数据区。
代码段:代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存种的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。
数据段:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。
BSS段:BSS段包含了程序中未初始化全局变量,在内存中bss段全部置零。
堆(heap):堆是用于存放进程运行中被动态分配的内存段,它大小并不固定,可动态扩张或缩减。当进程调用malloc/new等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈:栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味这在数据段中存放变量)。除此以外在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也回被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上将我们可以把堆栈看成一个临时数据寄存、交换的内存区。
我们要知道,栈中存放的是一个个被调函数所对应的堆栈帧,当函数fun1被调用,则fun1的堆栈帧入栈,fun1返回时,fun1的堆栈帧出栈。什么是堆栈帧呢,堆栈帧其实就是保存被调函数返回时下一条执行指令的指针、主调函数的堆栈帧的指针、主调函数传递给被调函数的实参(如果有的话)、被调函数的局部变量等信息的一个结构。
首先,我们要说明的是如何区分每个堆栈帧,或者说,如何知道我现在在使用哪个堆栈帧。和栈密切相关的有2个寄存器,一个是ebp,一个是esp,前者可以叫作栈基址指针,后者可以叫栈顶指针。对于一个堆栈帧来说,ebp也叫堆栈帧指针,它永远指向这个堆栈帧的某个固定位置(见上图),所以可以根据ebp来表示一个堆栈帧,可以通过对ebp的偏移加减,来在堆栈帧中来来回回的访问。esp则是随着push和pop而不断移动。因此根据esp来对堆栈帧进行操作。
再来讲一下上图,一个堆栈帧的最顶部,是实参,然后是return address,这个值是由主调函数中的call命令在call调用时自动压入的,不需要我们关心,previousframe pointer,就是主调函数的堆栈帧指针,也就是主调函数的ebp值。ebp偏移为正的都是被调函数的局部变量。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
C++程序内存布局探讨(一)
刘昕 重庆大学软件学院
摘要:
本文探讨了C++程序内存布局的基础知识,对堆、栈、全局数据区和代码区的概念进行了简单介绍,并介绍了内存对齐和进程地址空间(虚拟内存)方面的知识。
今天一大早起来,收到外校的同学传给我的一道C++面试题,该公司做Windows平台下的C++开发。面试题有一道考C++程序内存布局,很具有代表性。
已知有这样一段代码:
- #include <iostream>
- #include <string>
- using std::string;
- using std::cout;
- using std::endl;
- int global_a = 5; //全局对象
- static global_b = 6; //全局静态对象
- int main()
- {
- int a = 5; //声明一个变量5
- char b = 'a';
- int c = 8;
- static int d = 7;
- cout<<&a<<endl;
- cout<<&c<<endl;
- cout<<&d<<endl;
- cout<<&global_a<<endl;
- cout<<&global_b<<endl;
- return 0;
- }
问题如下:这段代码运行后会打印出变量的地址(这个很明显),以变量a为例,编译后第一次执行和第二次执行打印出来的变量地址是否一样?如果重新编译再次运行,打印出来的变量地址会不会变化?请解释原因。
我拿这道题问了几位同学,得出的答案主要有以下两种,第一种认为三次打印出来的变量a的地址都不一样,第二种认为前两次打印出来的变量a的地址一样,第三次打印出来的变量a的地址与前两次不同。
那么实际的结果是如何呢?在VC++6.0和VS2008下分别进行实验,请看三次程序运行的截图:
图一:VC6.0下程序运行截图
图二:VC++2008下运行截图
说明了什么?首先说明了我问的几位同学都提供的是错误的答案,再就是为什么会一样呢?一样会不会导致内存冲突(貌似而且实际上也是内存地址是一样的)。要解释这两个问题,必须了解C++程序的内存模型和Windows平台下的内存管理机制。
首先看C++程序的内存模型(本文不讨论C++对象内存模型),下面是一张经典的C++内存模型图:
图三:C++程序内存布局
从上面的图示中,可以建立起对C++程序运行的一个整体认识,共分为栈、堆、全局数据区和程序代码区。文首程序中的变量a便是局部变量,所以被分配在栈区。大家回忆一下栈的知识,一个栈顶指针,栈从高地址向低地址生长,栈有大小限制。好了,问题来了,为什么三次打印出来的对象a的地址一样。
首先,在VC++6.0下,在“a=5”这行语句上加一个断点,按F5j进入调试模式,然后按ALT+8查看编译器生成的汇编代码:
图四:汇编代码
这里面ebp就是当前的栈顶地址,注意到a占用三个字节,而且答应出来的a的地址实际上是a低字节的地址(回忆下计算机组成结构的知识)。那好,在第一个数据a入栈前的栈顶指针是多少呢?计算一下0012FF7C+4=0012FF80,这个值是由编译器决定的,从上面可以看出,VC++6.0下和VC++2008下这个值是不一样的。同理,也可以计算出全局数据区的起始地址。
好的,我们再深入一下,讨论另外一个问题,内存对齐。在声明变量a之后,又接着声明了字符b(占一个字节),最后又声明了整形变量c,那么c的地址应该是多少呢?计算一下:0012FF7C-1-4=0012FF77。但是实际上打印出来的是0012FF74。为什么呢?答案就是内存对齐。
在现代计算机体系结构中,为了使CPU对变量进行高效、快速地访问,变量的地址应该具有某种特性,那就是“对齐”,对齐行为是有编译器来来实施的。如本例中对于4个字节大小的整形变量,其起始地址应该位于4个字节边界上,即能够被4整除,汇编上的黑话叫做“模四地址”。
现在我们修改一下文首的代码,将动态对象加入,注意到动态对象是在堆中分配的,增加下面几行代码:
- int * pinteger = new int(5); // 在堆上分配内存
- cout<<pinteger<<endl;
- int * pinteger2 = new int(5);
- cout<<pinteger2<<endl;
- delete pinteger;
- delete pinteger2;
图五:增加了堆中的内存分配
注意到最后打印出的两个变量地址就是堆上的地址,注意到两个地址不是连续增长的。而在栈上和全局数据区分配的内存地址则是连续的。再就是注意到堆的地址在栈和全局变量区之间,这和上面所示的C++内存布局图示是一致的。
那好,问第二个问题,三个程序打印出来的变量地址一模一样,会不会发生地址冲突?答案是不会发生地址冲突,因为打印出来的地址不是变量的实际物理地址,而是虚拟地址,也叫逻辑地址。三个程序(实际上是同一个程序的三个运行实例)在操作系统中被当成三个独立的进程,彼此拥有独立的地址空间。它们之间互不影响,比如同样地址为0012FF7C的内存,在不同的进程中,它们的数据可能是完全不同的(在我们这个例子中是相同的)。在Windows操作系统中,通过虚拟内存机制,为每个进程提供了一个一致的内存视图,同时使得逻辑内存与物理内存分隔开来。虚拟内存的知识这里不再赘述,推荐《操作系统概念》一书,对虚拟内存有完整的介绍。
附录:
以下三段内容引用至李先静老师的博客,对虚拟内存的实现机制做了一个浅显易懂的介绍:
要做到进程的地址空间独立,光靠软件是难以实现的,通常还依赖于硬件的帮助。这种硬件称为MMU(Memory Manage Unit),即所谓的内存管理单元。在这种体系结构下,内存分为物理内存和虚拟内存两种。物理内存就是实际的内存,机器上装了多大内存条就有多大的物理内存。而应用程序使用的是虚拟内存,访问内存数据时,由MMU根据页表把虚拟内存地址转换对应的物理内存地址。
MMU把各个进程的虚拟内存映射到不同的物理内存上,这样就能保证进程的虚拟内存地址是独立的。由于物理内存远远少于各个进程的虚拟内存的总和,操作系统会把暂时不用的内存数据写到磁盘上去,把腾出来的物理内存分配给有需要的进程使用。一般会创建一个专门的分区存放换出的内存数据,这个分区称为交换分区。
从虚拟内存到物理内存的映射并不是一个字节一个字节映射的,而是以一个称为页(page)最小单位的进行的。页的大小视具体硬件平台而定,通常是 4K。当应用程序访问的虚拟内存的页面不在物理内存里时,MMU产生一个缺页中断,并挂起当前进程,缺页中断处理函数负责把相应的数据从磁盘读入内存,然后唤醒挂起的进程。
==========================================================分隔线==========================================================
结合内存分布图分析内存问题
1.内存问题的原因及分类
在C/C++程序中,有关内存使用的问题是最难发现和解决的。这些问题可能导致程序莫名其妙地停止、崩溃,或者不断消耗内存直至资源耗尽。由于C/C++语言本身的特质和历史原因,程序员使用内存需要注意的事项较多,而且语言本身也不提供类似Java的垃圾清理机制。编程人员使用一定的工具来查找和调试内存相关问题是十分必要的。
总的说来,与内存有关的问题可以分成两类:内存访问错误和内存使用错误。内存访问错误包括错误地读取内存和错误地写内存。错误地读取内存可能让你的模块返回意想不到的结果,从而导致后续的模块运行异常。错误地写内存可能导致系统崩溃。内存使用方面的错误主要是指申请的内存没有正确释放,从而使程序运行逐渐减慢,直至停止。这方面的错误由于表现比较慢很难被人工察觉。程序也许运行了很久才会耗净资源,发生问题。
1.1 内存解剖
一个典型的C++内存布局如下图所示:
自底向上,内存中依次存放着只读的程序代码和数据,全局变量和静态变量,堆中的动态申请变量和堆栈中的自动变量。自动变量就是在函数内声明的局部变量。当函数被调用时,它们被压入栈;当函数返回时,它们就要被弹出堆栈。堆栈的使用基本上由系统控制,用户一般不会直接对其进行控制,所以堆栈的使用还是相对安全的。动态内存是一柄双刃剑:它可以提供程序员更灵活的内存使用方法,而且有些算法没有动态内存会很难实现;但是动态内存往往是内存问题存在的沃土。
1.2 内存访问错误
相对用户使用的语言,动态内存的申请一般由malloc/new来完成,释放由free/delete完成。基本的原则可以总结为:一对一,不混用。也就是说一个malloc必须对应一且唯一的free;new对应一且唯一的delete; malloc不能和delete, new不能和free对应。另外在C++中要注意delete和delete[]的区别。delete用来释放单元变量,delete[]用来释放数组等集聚变量。有关这方面的详细信息可以参考[C++Adv]。
我们可以将内存访问错误大致分成以下几类:数组越界读或写、访问未初始化内存、访问已经释放的内存和重复释放内存或释放非法内存。
下面的代码集中显示了上述问题的典型例子:
1 #include <iostream> 2 using namespace std; 3 int main(){ 4 char* str1="four"; 5 char* str2=new char[4]; //not enough space 6 char* str3=str2; 7 cout<<str2<<endl; //UMR 8 strcpy(str2,str1); //ABW 9 cout<<str2<<endl; //ABR 10 delete str2; 11 str2[0]+=2; //FMR and FMW 12 delete str3; //FFM 13 }
由以上的程序,我们可以看到:在第5行分配内存时,忽略了字符串终止符"\0"所占空间导致了第8行的数组越界写(Array Bounds Write)和第9行的数组越界读(Array Bounds Read); 在第7行,打印尚未赋值的str2将产生访问未初始化内存错误(Uninitialized Memory Read);在第11行使用已经释放的变量将导致释放内存读和写错误(Freed Memory Read and Freed Memory Write);最后由于str3和str2所指的是同一片内存,第12行又一次释放了已经被释放的空间 (Free Freed Memory)。
这个包含许多错误的程序可以编译连接,而且可以在很多平台上运行。但是这些错误就像定时炸弹,会在特殊配置下触发,造成不可预见的错误。这就是内存错误难以发现的一个主要原因。
1.3 内存使用错误
内存使用错误主要是指内存泄漏,也就是指申请的动态内存没有被正确地释放,或者是没有指针可以访问这些内存。这些小的被人遗忘的内存块占据了一定的地址空间。当系统压力增大时,这些越来越多的小块将最终导致系统内存耗尽。内存使用错误比内存访问错误更加难以发现。这主要有两点原因:第一,内存使用错误是"慢性病",它的症状可能不会在少数、短时间的运行中体现;第二,内存使用错误是因为"不做为"(忘记释放内存)而不是"做错"造成的。这样由于忽略造成的错误在检查局部代码时很难发现,尤其是当系统相当复杂的时候。
- c程序内存布局
- C程序内存布局
- C程序内存布局
- c程序内存布局
- C程序内存布局
- C程序的内存布局
- C/C++程序内存布局
- C语言程序内存布局
- C语言程序内存布局
- C语言程序内存布局
- C程序的内存布局
- linux里面C程序内存布局
- 一个c程序的内存布局
- C语言程序的内存布局
- C语言程序的内存布局
- 字节序,C程序内存布局
- C语言程序的内存布局
- C语言程序的内存布局
- 设计模式之三 --- 策略模式(Strategy Pattern)
- 设计模式之一——Stratety
- get the week in year for a date use javascript
- 设计模式之四 --- 建造(Builder)模式
- 技术创业公司,技术团队一定要有一个强者坐镇,否则失败率极高
- C++程序内存布局
- 设计模式之五 --- 代理(Proxy)模式
- 移植Uboot-2010.06到TQ2440开发板详解之三
- 有关对耗时很大循环进行并行化优化的探讨 之一:并发搜索的处理
- Linux的三种线程实现模型漫谈
- 继承与派生——虚基类
- 编写安全代码——不要用memcmp比较structure
- lib和dll的关系
- chown更改文件和目录的所有者