精华内容
下载资源
问答
  • 2014-03-24 09:40:29

    按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的.
    静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.
    栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。
    静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.

    2.2 堆和栈的比较
    上面的定义从编译原理的教材中总结而来,除静态存储分配之外,都显得很呆板和难以理解,下面撇开静态存储分配,集中比较堆和栈:
    从堆和栈的功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的.而这种不同又主要是由于堆和栈的特点决定的:
    在编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)一样,Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快, 当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时.
    堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因,看来列宁同志说的好,人的优点往往也是人的缺点,人的缺点往往也是人的优点(晕~).
    2.3 JVM中的堆和栈 
    JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
    我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.
    从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。
    每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。


    2.4 GC的思考
    Java为什么慢?JVM的存在当然是一个原因,但有人说,在Java中,除了简单类型(int,char等)的数据结构,其它都是在堆中分配内存(所以说Java的一切都是对象),这也是程序慢的原因之一。
    我的想法是(应该说代表TIJ的观点),如果没有Garbage Collector(GC),上面的说法就是成立的.堆不象栈是连续的空间,没有办法指望堆本身的内存分配能够象堆栈一样拥有传送带般的速度,因为,谁会为你整理庞大的堆空间,让你几乎没有延迟的从堆中获取新的空间呢?
    这个时候,GC站出来解决问题.我们都知道GC用来清除内存垃圾,为堆腾出空间供程序使用,但GC同时也担负了另外一个重要的任务,就是要让Java中堆的内存分配和其他语言中堆栈的内存分配一样快,因为速度的问题几乎是众口一词的对Java的诟病.要达到这样的目的,就必须使堆的分配也能够做到象传送带一样,不用自己操心去找空闲空间.这样,GC除了负责清除Garbage外,还要负责整理堆中的对象,把它们转移到一个远离Garbage的纯净空间中无间隔的排列起来,就象堆栈中一样紧凑,这样Heap Pointer就可以方便的指向传送带的起始位置,或者说一个未使用的空间,为下一个需要分配内存的对象"指引方向".因此可以这样说,垃圾收集影响了对象的创建速度,听起来很怪,对不对?
    那GC怎样在堆中找到所有存活的对象呢?前面说了,在建立一个对象时,在堆中分配实际建立这个对象的内存,而在堆栈中分配一个指向这个堆对象的指针(引用),那么只要在堆栈(也有可能在静态存储区)找到这个引用,就可以跟踪到所有存活的对象.找到之后,GC将它们从一个堆的块中移到另外一个堆的块中,并将它们一个挨一个的排列起来,就象我们上面说的那样,模拟出了一个栈的结构,但又不是先进后出的分配,而是可以任意分配的,在速度可以保证的情况下, Isn't it great?
    但是,列宁同志说了,人的优点往往也是人的缺点,人的缺点往往也是人的优点(再晕~~).GC()的运行要占用一个线程,这本身就是一个降低程序运行性能的缺陷,更何况这个线程还要在堆中把内存翻来覆去的折腾.不仅如此,如上面所说,堆中存活的对象被搬移了位置,那么所有对这些对象的引用都要重新赋值.这些开销都会导致性能的降低.
    此消彼长,GC()的优点带来的效益是否盖过了它的缺点导致的损失,我也没有太多的体会,Bruce Eckel 是Java的支持者,王婆卖瓜,话不能全信.个人总的感觉是,Java还是很慢,它的发展还需要时间.


    上面的体会是我看了TIJ.3rdEdition.Revision4.0中第四章之后得出的,内容和前面的有些不同.我没有看过侯捷的中文版本,但我觉得,在关键问题上,原版的TIJ的确更值得一读.所以和中文版配合起来学习是比较不错的选择.
    我只能算一个Java的初学者,没想到起了这么个题目,却受到这么多人的关注,欣喜之余,也决心尽力写好下面的每一篇.不过这一篇完了,我就该准备赴美签证了,如果成功,那就要等到8月27号CS的研究生院开学之后,才有时间会开始研究下一章了,希望可以多从原版中获取一点经验.

    堆和栈的区别和联系

    一、简单的可以理解为: 
    heap:是由malloc之类函数分配的空间所在地。地址是由低向高增长的。 
    stack:是自动分配变量,以及函数调用的时候所使用的一些空间。地址是由高向低减少的。

    二、堆和栈的理论知识
    2.1申请方式
    stack:
    由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
    heap:
    需要程序员自己申请,并指明大小,在c中malloc函数
    如p1 = (char *)malloc(10);
    在C++中用new运算符
    如p2 = (char *)malloc(10);
    但是注意p1、p2本身是在栈中的。
    2.2
    申请后系统的响应
    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
    会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
    2.3申请大小的限制
    栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
    2.4申请效率的比较:
    栈由系统自动分配,速度较快。但程序员是无法控制的。
    堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
    另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度, 也最灵活
    2.5堆和栈中的存储内容
    栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
    当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
    堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
    2.6存取效率的比较

    char s1[] = "aaaaaaaaaaaaaaa";
    char *s2 = "bbbbbbbbbbbbbbbbb";
    aaaaaaaaaaa是在运行时刻赋值的;
    而bbbbbbbbbbb是在编译时就确定的;
    但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
    比如:
    #include 
    void main()
    {
    char a = 1;
    char c[] = "1234567890";
    char *p ="1234567890";
    a = c[1];
    a = p[1];
    return;
    }
    对应的汇编代码
    10: a = c[1];
    00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
    0040106A 88 4D FC mov byte ptr [ebp-4],cl
    11: a = p[1];
    0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
    00401070 8A 42 01 mov al,byte ptr [edx+1]
    00401073 88 45 FC mov byte ptr [ebp-4],al
    第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指edx中,在根据edx读取字符,显然慢了。


    2.7小结:
    堆和栈的区别可以用如下的比喻来看出:
    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
    使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

    堆和栈的区别主要分:
    操作系统方面的堆和栈,如上面说的那些,不多说了。
    还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。
    虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因而已。


    更多相关内容
  • 程序运行时存储结构

    千次阅读 2018-06-06 22:02:00
    程序运行时的存储结构 目标程序在目标机器环境下运行,只是在自己的运行存储空间内完成执行。通常,在有操作系统的情况下,程序在自己的逻辑存储空间内存储和运行。因此,编译程序在生成目标代码应该明确...

    程序运行时的存储结构

    目标程序在目标机器环境下运行时,只是在自己的运行时的存储空间内完成执行。通常,在有操作系统的情况下,程序在自己的逻辑存储空间内存储和运行。因此,编译程序在生成目标代码时应该明确程序的所有对象在逻辑存储空间是如何存储的,以及目标代码运行时是如何使用和分配自己的逻辑存储空间的。

    –摘自《编译原理》第三版 王生原等编著。

    :本文中例子以c语言为例。

    运行时存储组织

    运行时存储组织的作用和任务

    编译程序所产生的目标程序本身的大小通常是固定的,一般情况下放在代码区

    目标程序所需要的数据对象放在数据区。这里的数据对象既包括用户定义的各种变量或者常量(比如:int a; 其中的a就是一个数据对象),也包括调用过程中所需的连接信息(比如:printf("hello world");,代码区也保存你调用的printf的函数的相关信息)。

    数据对象的表示

    明确源语言中各类数据对象在目标机器中的表示形式,其实就是确定具体的存储方案。

    数据对象在目标机器中通常是以字节(Byte)为单位进行分配,同一类型的数据对象,在不同类型的目标机器字节数可能不同。

    对于基本数据对象,比如说integer 通常占4个字节,character占一个字节,而指针类型的数据对象占一个单位字长(单位字长由目标机器位数决定)。

    表达式计算

    如何正确有效的组织表达式的计算,其中涉及到代码优化等问题。

    存储分配策略

    采取静态分配还是动态分配。静态分配(比如:c中打的static)是指编译时就确定数据对象的大小,个数。动态分配(比如c中的malloc或者c++和java中的new)是指运行时才能确定对象的存储分配。

    过程实现

    如何实现函数调用,参数传递,返回值等问题。

    程序运行时存储空间的布局

    粗略划分可以把程序的存储布局分为代码区和数据区,但是为了存储组织和管理,往往需要将它们划分为更加具体的区域。具体的划分因目标机器而定,但是一般来说,可以划分为以下几个部分:

    • 保留地址区。为操作系统保留的地址区域等。通常是不允许普通程序的读写,只允许操作系统的特权操作读写。
    • 代码区。静态存放编译产生的目标代码。
    • 静态数据区。静态存放全局数据。比如说用的全局变量以及静态变量等。
    • 共享库和分别编译模块。
    • 动态数据区。指的是动态变化的堆区和栈区。堆(数据对象由低地址–>高地址存储)。栈(数据对象由高地址–>低地址存储)。

    这里写图片描述

    栈式存储分配

    采用来存储和管理。

    在函数/过程的实现中,每次调用函数/过程时,就在当前栈顶加一个活动记录(activity record)。当函数执行完毕后,释放掉当前函数的栈顶记录。

    这也是为什么会有变量的域概念这一说法,函数的局部变量,只在当前函数域内有效,执行完当前函数,就会释放掉当前函数域。

    #include <stdio.h>
    //计算两个数的加法
    int sum(int a, int b)
    {
        int c;
        c = a+b;
    
        return a+b;
    }
    int main(void)
    {
        int a;
        int b;
        int c;
        c = sum(a,b);
        //打印计算结果
        printf("%d+%d = %d",a,b,c);
    
        return 1;
    }

    当执行上面的程序时:

    1. 程序从main函数开始执行
    2. 系统为main函数分配栈活动记录,将main函数的活动记录压进栈(push stack),当前栈顶指针指向main函数函数记录。
    3. 当函数执行到15行时,将开始调用sum函数
    4. 系统为sum函数分配栈活动记录,将sum函数的活动记录压进栈(push stack),当前栈顶指针指向sum函数活动记录。
    5. 执行完sum函数,将当前栈顶记录弹出(pop stack),当前栈顶指针指向main函数的活动记录。
    6. 将sum函数的返回值赋值给c。
    7. 函数数执行到17行时,调用系统I/O函数printf。
    8. 系统为printf函数分配栈活动记录,将printf函数的活动记录压进栈(push stack),当前栈顶指针指向printf函数的活动记录。
    9. 执行完printf函数,将当前栈顶记录弹出(pop stack),当前栈顶指针指向main函数的函数记录。
    10. 系统将main函数的活动记录弹出,程序执行完毕。
    什么情况下才能该用栈式存储?

    在编译时期,过程/函数以及嵌套程序块的活动记录大小(最大值)是可以确定的。

    堆式存储分配

    从栈式存储中,我们知道,每次函数执行完毕后,都会将当前栈顶活动记录弹出,这意味着变量随着函数执行的结束而丢失。但是有时候我们想要函数结束完之后,当前数据对象长期存在,这时候就该堆发挥作用了。

    在堆式存储分配时,程序可以在任意时刻,以任意次序从程序的代码区的堆区申请和释放一个数据对象。通常情况下,堆的分配和释放是由程序向操作系统提出的,这也注定了它的分配效率没有栈那么快。

    堆的分配和释放有两种方式:隐式(implicit)和显式(explicit)。

    在c语言中没有显式的分配和释放堆的语句,但是可以用标准库中的malloc函数和free函数实现,这需要程序员的水平比较高。在java语言中,堆的释放都是有Java的垃圾回收器来隐式释放的而且这个垃圾回收器相当’聪明‘这也降低了java的入门门槛。

    展开全文
  • 程序运行时内存空间分布

    万次阅读 多人点赞 2014-03-21 01:33:18
    一、程序运行时内存空间情况 其实在程序运行时,由于内存的管理方式是以页为单位的,而且程序使用的地址都是虚拟地址,当程序要使用内存时,操作系统再把虚拟地址映射到真实的物理内存的地址上。所以在程序中,...
    我们在写程序时,既有程序的逻辑代码,也有在程序中定义的变量等数据,那么当我们的程序进行时,我们的代码和数据究竟是存放在哪里的呢?下面就来总结一下。

    一、程序运行时的内存空间情况

    其实在程序运行时,由于内存的管理方式是以页为单位的,而且程序使用的地址都是虚拟地址,当程序要使用内存时,操作系统再把虚拟地址映射到真实的物理内存的地址上。所以在程序中,以虚拟地址来看,数据或代码是一块块地存在于内存中的,通常我们称其为一个段。而且代码和数据是分开存放的,即不储存于同于一个段中,而且各种数据也是分开存放在不同的段中的。

    下面以一个简单的程序来看一下在Linux下的程序运行空间情况,代码文件名为space.c
    #include <unistd.h>
    #include <stdio.h>
    
    int main()
    {
    	printf("%d\n", getpid());
    	while(1);
    	return 0;
    }

    这个程序非常简单,输出当前进程的进程号,然后进入一个死循环,这个死循环的目的只是让程序不退出。而在Linux下有一个目录/proc/$(pid),这个目录保存了进程号为pid的进程运行时的所有信息,其中有一个文件maps,它记录了程序执行过程中的内存空间的情况。编译运行上面的代码,其运行结果如图1所示:


    从上面的图中,我们可以看到这样一个简单的程序,在执行时,需要哪些库和哪些空间。上面的图的各列的意思,不一一详述,只对重要的进行说明。
    第一列的是一个段的起始地址和结束地址,第二列这个段的权限,第三列段的段内相对偏移量,第六列是这个段所存放的内容所对应的文件。从上图可以看到我们的程序进行首先要加载系统的两个共享库,然后再加载我们写的程序的代码。

    对于第二列的权限,r:表示可读,w:表示可写,x:表示可执行,p:表示受保护(即只对本进程有效,不共享),与之相对的是s,意是就是共享。

    从上图我们可以非常形象地看到一个程序进行时的内存分布情况。下面我们将会结合上图,进行更加深入的对内存中的数据段的解说。

    二、程序运行时内存的各种数据段

    1.bss段
    该段用来存放没有被初始化或初始化为0的全局变量,因为是全局变量,所以在程序运行的整个生命周期内都存在于内存中。有趣的是这个段中的变量只占用程序运行时的内存空间,而不占用程序文件的储存空间。可以用以下程序来说明这点,文件名为bss.c
    #include <stdio.h>
    
    int bss_data[1024 * 1024];
    
    int main()
    {
    	return 0;
    }
    这个程序非常简单,定义一个4M的全局变量,然后返回。编译成可执行文件bss,并查看可执行文件的文件属性如图2所示:


    从可执行文件的大小4774B可以看出,bss数据段(4M)并不占用程序文件的储存空间,在下面的data段中,我们可以看到data段的数据是占用可执行文件的储存空间的。

    在图1中,有文件名且属性为rw-p的内存区间,就是bss段。

    2.data段
    初始化过的全局变量数据段,该段用来保存初始化了的非0的全局变量,如果全局变量初始化为0,则编译有时会出于优化的考虑,将其放在bss段中。因为也是全局变量,所以在程序运行的整个生命周期内都存在于内存中。与bss段不同的是,data段中的变量既占程序运行时的内存空间,也占程序文件的储存空间。可以用下面的程序来说明,文件名为data.c:
    #include <stdio.h>
    
    int data_data[1024 * 1024] = {1};
    
    int main()
    {
    	return 0;
    }
    这个程序与上面的bss唯一的不同就是全局变量int型数组data_data,其中第0个元素的值初始化为1,其他元素的值初始化成默认的0,而因为数组的地址是连续的,所以只要有一个元素在data段中,则其他的元素也必然在data段中。编 译连接成可执行文件data,并查看可执行文件的文件属性如图3所示:


    从可执行文件的大小来看,data段数据(data_data数组的大小,4M)占用程序文件的储存空间。

    在图1中,有文件名且属性为rw-p的内存区间,就是data段,它与bss段在内存中是共用一段内存的,不同的是,bss段数据不占用文件,而data段数据占用文件储存空间。

    3.rodata段
    该段是常量数据段,用于存放常量数据,ro就是Read Only之意。但是注意并不是所有的常量都是放在常量数据段的,其特殊情况如下:
    1)有些立即数与指令编译在一起直接放在代码段(text段,下面会讲到)中。
    2)对于字符串常量,编译器会去掉重复的常量,让程序的每个字符串常量只有一份。
    3)有些系统中rodata段是多个进程共享的,目的是为了提高空间的利用率。

    在图1中,有文件名的属性为r--p的内存区间就是rodata段。可见他是受保护的,只能被读取,从而提高程序的稳定性。

    4.text段
    text段就是代码段,用来存放程序的代码(如函数)和部分整数常量。它与rodata段的主要不同是,text段是可以执行的,而且不被不同的进程共享。

    在图1中,有文件名且属性为r-xp的内存区间就是text段。就如我们所知道的那样,代码段是不能被写的。

    5.stack段
    该段就是栈段,用来保存临时变量和函数参数。程序中的函数调用就是以栈的方式来实现的,通常栈是向下(即向低地址)增长的,当向栈中push一个元素,栈顶指针就会向低地址移动,当从栈中pop一个元素,栈顶指针就会向高地址移动。栈中的数据只在当前函数或下一层函数中有效,当函数返回时,这些数据自动被释放,如果继续对这些数据进行访问,将发生未知的错误。通常我们在程序中定义的不是用malloc系统函数或new出来的变量,都是存放在栈中的。例如,如下函数:
    void func()
    {
        int a = 0;
        int *n_ptr = malloc(sizeof(int));
        char *c_ptr = new char;
    }

    整型变量a,整型指针变量n_ptr和char型指针变量c_ptr,都存放在栈段中,而n_ptr和c_ptr指向的变量,由于是malloc或new出来的,所以存放在堆中。当函数func返回时,a、n_ptr、c_ptr都会被释放,但是n_ptr和c_ptr指向的内存却不会释放。因为它们是存在于堆中的数据。

    在图1中,文件名为stack的内存区间即为栈段。

    6.heap段
    heap(堆)是最自由的一种内存,它完全由程序来负责内存的管理,包括什么时候申请,什么时候释放,而且对它的使用也没有什么大小的限制。在C/C++中,用alloc系统函数和new申请的内存都存在于heap段中。

    以上面的程序为例,它向堆申请了一个int和一个char的内存,因为没有调用free或delete,所以当函数返回时,堆中的int和char变量并没有释放,造成了内存泄漏。

    由于在图1所对应的代码中没有使用alloc系统函数或new来申请内存,所以heap段并没有在图1中显示出来,所以以下面的程序来说明heap段的位置,代码文件为heap.c,代码如下:
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    int main()
    {
    	int *n_ptr = malloc(sizeof(int));
    	printf("%d\n", getpid());
    	while(1);
    	free(n_ptr);
    	return 0;
    }
    查看其运行时内存空间分布如下:


    可以看到文件名为heap的内存区间就是heap段。从上图,也可以看出,虽然我们只申请4个字节(sizeof(int))的空间,但是在操作系统中,内存是以页的方式进行管理的,所以在分配heap内存时,还是一次分配就为我们分配了一个页的内存。注:无论是图1,还是上图,都有一些没有文件名的内存区间,其实没用文件名的内存区间表示使用mmap映射的匿名空间。

    展开全文
  • C++程序运行时内存与地址

    千次阅读 2022-03-15 17:24:13
    本文全部内容,以SSD6的Exercise1为示例。 代码如下图。 #include <stdio.h> #include <stdlib.h> #include <... 0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F, ... 0x63636363, 0x63636363

    本文全部内容,以SSD6的Exercise1为示例。

    代码如下图。

    #include <stdio.h>
    #include <stdlib.h>
    #include <iostream>
    
    int prologue [] = {
    	0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F,
    	0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
    	0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
    	0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
    	0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
    	0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
    	0x20206F74, 0x74786565, 0x65617276, 0x32727463,
    	0x594E2020, 0x206F776F, 0x79727574, 0x4563200A
    };
    
    int data [] = {
    	0x63636363, 0x63636363, 0x72464663, 0x6F6D6F72,
          	0x466D203A, 0x65693A72, 0x43646E20, 0x6F54540A,
          	0x5920453A, 0x54756F0A, 0x6F6F470A, 0x21643A6F,
          	0x594E2020, 0x206F776F, 0x79727574, 0x4563200A,
          	0x6F786F68, 0x6E696373, 0x6C206765, 0x796C656B,
          	0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
          	0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
          	0x20206F74, 0x74786565, 0x65617276, 0x32727463,
          	0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
          	0x21687467, 0x63002065, 0x6C6C7861, 0x78742078,
          	0x6578206F, 0x72747878, 0x78636178, 0x00783174
    };
    
    int epilogue [] = {
    	0x594E2020, 0x206F776F, 0x79727574, 0x4563200A,
    	0x6E617920, 0x680A6474, 0x6F697661, 0x20646E69,
    	0x7565636F, 0x20206120, 0x6C616763, 0x74206C6F,
    	0x2C336573, 0x7420346E, 0x20216F74, 0x726F5966,
    	0x20206F74, 0x74786565, 0x65617276, 0x32727463
    };
    
    char message[150];
    
    void usage_and_exit(char * program_name) {
    	fprintf(stderr, "USAGE: %s key1 key2 key3 key4\n", program_name);
    	exit(1);
    }
    
    void process_keys12 (int * key1, int * key2) {
    	
    	*((int *) (key1 + *key1)) = *key2;
    }
    
    void process_keys34 (int * key3, int * key4) {
    
    	*(((int *)&key3) + *key3) += *key4;
    }
    
    char * extract_message1(int start, int stride) {
    	int i, j, k;
    	int done = 0;
    
    	for (i = 0, j = start + 1; ! done; j++) {
    		for (k = 1; k < stride; k++, j++, i++) {
    
    			if (*(((char *) data) + j) == '\0') {
    				done = 1;
    				break;
    			}
    							 
    			message[i] = *(((char *) data) + j);
    		}
    	}
    	message[i] = '\0';
    	return message;
    }
    
    
    char * extract_message2(int start, int stride) {
    	int i, j;
    
    	for (i = 0, j = start; 
    		 *(((char *) data) + j) != '\0';
    		 i++, j += stride) 
    		 {
    			 message[i] = *(((char *) data) + j);
    		 }
    	message[i] = '\0';
    	return message;
    }
    
    int main (int argc, char *argv[])
    {
    	int dummy = 1;
    	int start, stride;
    	int key1, key2, key3, key4;
    	char * msg1, * msg2;
    
    	int* p = &dummy;
    	int* q = &key1;
    	key3 = key4 = 0;
    	if (argc < 3) {
    		usage_and_exit(argv[0]);
    	}
    	key1 = strtol(argv[1], NULL, 0);
    	key2 = strtol(argv[2], NULL, 0);
    	if (argc > 3) key3 = strtol(argv[3], NULL, 0);
    	if (argc > 4) key4 = strtol(argv[4], NULL, 0);
    
    	process_keys12(&key1, &key2);
    
    	start = (int)(*((char *) &dummy));
    	stride = (int)(*(((char *) &dummy) + 1));
    
    	if (key3 != 0 && key4 != 0) {
    		process_keys34(&key3, &key4);
    	}
    
    	msg1 = extract_message1(start, stride);
    
    	if (*msg1 == '\0') {
    		process_keys34(&key3, &key4);
    		msg2 = extract_message2(start, stride);
    		printf("%s\n", msg2);
    	}
    	else {
    		printf("%s\n", msg1);
    	}
    
    	return 0;
    }
    

    首先看刚进入main函数的前几个int变量。

    int dummy = 1;
    int start, stride;
    int key1, key2, key3, key4;

    理论上来说他们的地址空间应该是连续的,那么实际上如何呢?

    每个变量相差12个字节,也就是3个int,居然没有挨着。

    再看看内存呢。

     

     可以看到的确是每个int相隔12字节,这个是编译器自己的优化,我们暂且不讨论。

    但可以知道的是,我们能通过key1的地址,找到dummy的地址,并修改dummy的值。

    dummy一旦被修改,start和stride也会被赋相应的值, 如此可得到message1.

    再看后面,如何通过key3,key4,使程序跳过message1执行message2?

    这里涉及到函数的运行机制。


    概念

    首先,明确几个概念。

    ESP(Extended Stack Pointer):为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。

    EBP(Extended Base Pointer):扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针。

    活跃记录(Activation Records and Stacks):栈顶的那个记录被称作活跃记录,也就是说在函数栈中,只有栈顶的记录可被程序指令访问和操作。

    函数帧:执行函数所需要的信息集合,具体内容后面会提到。

    程序当前地址:程序运行时当前指令所在地址,也是PC(Program Counter)程序地址寄存器中的值。

    函数返回地址:函数执行完毕后应返回上一层函数继续执行指令的地址,也是指令地址。

    函数调用过程

    如上图所示 ,main函数中要调用两个函数,.1和2。左图中此时mian函数已经调用了2,当前的栈顶活跃记录为函数2,注意观察左图中有三个箭头,他们代表着在函数调用过程中函数栈上的三个关键指针,FramePointer对应ebp,StackPointer对应esp,或者说这两个指针的值分别存储在这两个寄存器中,另外一个没有名字的箭头,为上一层函数的FramePointer,当函数返回时,通过它来恢复调用该函数前的函数栈的信息。

    再看右图,此时函数2调用函数1,此时发生的事件是:

    1,保存当前FramePointer,一般保存在一个专用栈中。

    2,将FramePointer指向StackPointer指向的地址。

    3,将StackPointer指向栈顶。

    函数返回过程

     返回过程发生的事件为:

    1,将StackPointer指向FramePointer指向的地址。

    2,将专用栈中保存的上一层FramePointer出栈。

    以上是有关函数调用和返回的简易理解,下面是详细的细节。

    函数调用过程的变量

     

     上图中有两个函数,bar为调用方,baz为被调用方。

    在图一中,可以看到在真正进入baz之前,我们的指令还做了两件事:

    1,将函数的参数压入函数栈。

    2,将函数的返回地址压入栈中。

    关于返回地址:可以看到函数的返回地址指向的是pop b指令,这条指令在call baz之后,含义是,返回地址所存储的地址值,是调用该函数的指令的下一条指令的地址,这很好理解,毕竟执行完函数还是需要回到被调用方继续执行接下来的指令。

    关于函数参数:这里只有一个参数,当函数参数大于等于二得时候,参数列表的顺序和他们入栈的顺序是相反的。

    更详细的信息是,函数内部所需要的局部变量存在哪里?

     

    关于局部变量:由图可知,局部变量是按照顺序(不同的编译器可能不一样,但一定是在EBP和ESP之间)存在EBP和ESP之间的,并且空白的部分用0xCCCCCCCC填充,这也是为什么我们查看内存的时候经常会看到一连串的C的原因。

     到这里所有函数调用相关的知识已经介绍完了,回过头来看问题。


    我们可以通过key3,key4来修改process_key34的返回地址来达到执行message2的目的。

    通过上面学习到的知识我们知道函数返回地址和参数是挨着的,并且参数列表入栈的顺序和列表序是相反的,又知道是参数先入栈,返回地址后入栈,所以返回地址刚好在key3的下面,如下图所示。

    这里写图片描述

     至于为什么是栈是从高地址向低地址进行的,这个是规定,记住就好。

    那么偏移量显而易见了。

    但是我们要将返回地址改成什么呢? 改成message2之前的process_key34地址就对了。

    这里我们只能查看反汇编,查看两个process_key34结束地址的偏移量来确定二者的偏移。

    到这里这道题就可以解开啦。


    总结

    • 指令地址和数据地址是不一样的,不要把PC和esp,ebp这种混为一谈;返回地址返回的是PC,而esp和epb控制的是栈指针,需要区分开。
    • 总体上来说,程序运行的整个过程,是由指令来控制的,但是程序需要调用各种函数,这时就需要使用栈来管理,而esp,ebp,返回地址都是为管理这个栈和保证程序顺利运行来服务的,并且在esp和ebp之间会存有局部变量,在活跃记录的生命周期中存活。
    • 不同的编译器数据的间隔长度可能不同,要根据实际测试的情况解题。
    展开全文
  • 程序执行内存分配

    千次阅读 2021-01-15 09:35:16
    一、在程序执行期间,变量存储空间有三种: 1、静态存储区。内存在程序编译的时候就已经分配好了,这块内存在程序执行期间都存在, 存储全局变量和静态变量。 2、栈存储区。内存是在程序执行期间才分配的,函数内...
  • matlab查看程序运行占用的内存

    千次阅读 2021-06-02 18:40:48
    feature('memstats')
  • 1 静态数据区 2 代码区 3 堆区 4 栈区
  • 程序所占用的手机存储空间,主要有以下两个部分:首次加载小程序时,微信从服务器上下载的小程序本体。小程序运行过程中,存放至本地的数据。不过不用担心, 微信团队早已帮你想好存储空间的问题了。首先,微信...
  • 1.程序计数器是一块较小的内存空间,几乎可以忽略; 2.是当前线程所执行的字节码的行号指示器; 3.Java多线程执行,每条线程都会有一个独立的程序计数器,各条线程之间互不影响; 4.该区域是“线程私有”的内存...
  • java程序运行时jvm内存分配

    千次阅读 2017-05-03 14:59:36
    概述 众所周知,在内存管理方面,对于从事C,C++的开发人员来说,他们是内存管理方面的“上帝”,负责着每一个对象生命开始到结束,这样一定程度上给程序员增加了很多麻烦(每个new操作都要写...接下来说说程序运行...
  • 编译后程序运行时内存中的堆栈分布,局部变量、全局变量、堆、堆栈、静态和全局
  • 在matlab命令窗口执行 feature('memstats')可以2113查看:5261物理内存:使用4102,可用,总数交换页面:使用,可用,总数虚拟内1653存:使用,可用,总数最大连续可用内存块:按从大到小排序,以及总数命令 whos 和...
  • Java程序内存运行详解

    千次阅读 2021-01-03 21:08:05
    先了解下 JVM 的内存分布,因为Java程序想要运行,就要依靠 JVM,可以把JVM理解成Java程序和操作系统之间的桥梁,JVM 实现了Java 的平台无关性,由此可见JVM的重要性。所以在学习 Java 内存分配原理的时候一定要牢记...
  • 使用windows API计算程序内存和时间消耗 获取内存使用量 获取内存使用量主要使用Psapi.h中声明的GetProcessMemoryInfo函数: 引入头文件: #include <windows.h> #include <psapi.h> #pragma comment...
  • 对pycharm 修改程序运行所需内存详解

    千次阅读 2020-12-08 20:35:46
    XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow 以上这篇对pycharm 修改程序运行所需内存详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。 本文标题: 对...
  • 就是自己在做一些ACM的题目然后有些题目内存会超,有没有什么方法可以测试自己写的程序到底占用了多大的内存呢? 我用指针实现的树结构,然后标程是用数组实现的,数组占得内存比指针少吗?
  • JVM运行时内存结构

    千次阅读 2019-02-26 08:20:52
    内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配; 方法区存储类信息、常量、静态变量等数据,...
  • 程序是怎么装载到内存并被运行

    千次阅读 2021-03-17 17:10:42
    在后续所有内容之前,我们需要先达成一个共识,所有的程序都是被装载进内存然后才被使用的。装载器会把对应的指令和数据加载到内存里面来,让 CPU 去执行,而程序,包括操作系统就是一堆指令和数据的集合。 下面...
  • 动态存储分配 有些操作对象只有在程序运行时才能确定这样编译器在编译就无法为他们预定存储空间只能在程序运行时系统根据运行的要求进行内存分配称为动态存储分配 当程序运行到需要一个动态分配的变量或对象...
  • x86的pc机cpu在运行的时候程序存储在RAM中的,而单片机等嵌入式系统则是存于flash中   x86cpu和单片机读取程序的具体途径  pc机在运行程序的时候将程序从外存(硬盘)中,调入到RAM中运行,cpu从RAM中读取...
  • 说起手机的运行内存,你可能不知道是什么,说起RAW,想必大家都觉得眼熟,没错,这就是...什么是运行内存在一部手机常见的存储部件中,RAM存储器,也就是运行内存,它的作用相当于电脑的内存条,不仅能存储东西,...
  • 4行Python代码监测每行程序运行时间和空间消耗

    千次阅读 多人点赞 2020-04-19 11:20:19
    Python是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言,其具有高可扩展性和高可移植性,具有广泛的标准库,受到开发者的追捧...所以这更加需要开发者在使用Python语言开发项目协调好程序运行的...
  • 在jupyter中运行程序后,点击kernel中的shutdown。如下图所示 或者 当然,在一开始的时候加上如下代码,进行显存管理是必要的: import tensorflow as tf import os os.environ["CUDA_VISIBLE_DEVICES"] = '...
  • 程序运行时硬件的工作流程

    千次阅读 2018-11-29 13:22:05
    1.当在终端输入命令行,由键盘控制器将信号流传至CPU中的寄存器,再由寄存器传至内存中 2.命令输入完成点击回车,系统会判定输入完成,调用程序文件这时存放在磁盘的文件会传至内存中 3.内存中的程序代码数据...
  • 可能没有想象的那么美好——微信小程序存储占用与清理实测2019-02-20 09:25:34313点赞922收藏149评论自从2017年微信小程序上线以来,从当初红极一的“跳一跳”到现在各种小程序囊括生活中的方方面面,观看新闻、...
  • 一个完整的Java程序运行过程会涉及以下内存区域:1、寄存器:JVM内部虚拟寄存器,存取速度非常快,程序不可控制。2、 栈:保存局部变量的值,包括: 1)用来保存基本数据类型的值; 2)保存类的实例,即堆区对象的...
  • 详细讲述三种引起C++程序运行过程中发生异常闪退的常见原因。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,192,704
精华内容 477,081
关键字:

程序运行时的内存空间