还剩10页未读,继续阅读
本资源只提供10页预览,全部文档请下载后查看!喜欢就下载吧,查找使用更方便
文本内容:
深度剖析堆与栈
一、预备学问一程序的内存安排一个由C/C++编译的程序占用的内存分为以下几个部分
1、栈区stack-由编译器自动安排释放,存放函数的参数值,局部变量的值等其操作方式类似于数据结构中的栈
2、堆区heap-一般由程序员安排释放,若程序员不释放,程序结束时可能由OS回收留意它与数据结构中的堆是两回事,安排方式倒是类似于链表,呵呵
3、全局区静态区static全局变量和静态变量的存储是放在一块的初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域-程序结束后有系统释放
4、文字常量区一常量字符串就是放在这里的程序结束后由系统释放
5、程序代码区一存放函数体的二进制代码
二、例子程序这是一个前辈写的,特别具体//maincppinta=0;全局初始化区char*pl;全局未初始化区mainintb;栈chars[]=abc〃;栈char*p2;栈char*p3=123456〃;123456\0在常量区p3在栈上staticintc=0;全局静态初始化区pl=char*malloc10;p2=char*malloc20;安排得来得10和20字节的区域就在堆区strcpypl〃123456〃;123456\0放在常量区,编译器可能会将它与p3所指向调用,被编译成了如此繁杂的机器代码好了,下面我们再具体一点,以前面的C++程序为例,以VC反汇编断点调试的方式,看看以上过程是如何实现的在A处,进程的主线程调用了函数fn并传递了参数其汇编代码如下004010B6moveaxdwordptr[ebp-4]〃此时ebp=0x0012ff80i=0xl2ff7cebp-4为i的地址004010B9pusheax00401OBAcall@ILT+20(fn)
(00401019)再明显不过了,第一行汇编代码将变量i的数值放入寄存器eax中其次行汇编代码将变量i的数值压入栈中,对应前面
(1)第三行汇编代码执行call指令,此条指令自动将返回地址压入栈中,然后,跳转到函数体内部,预备执行函数内部的代码,对应前面
(2)现在我们再来看看函数内部的代码是什么样子这里只截取部分相关代码intfn(intn){00401050pushebp00401051movebpesp••••••n+=l;00401068moveaxdwordptr[ebp+8]〃此时ebp=0x12ff20n=0x!2ff28eax=l0040106Baddeax10040106Emovdwordptr[ebp+8]eax第一行汇编代码对应前面
(3)其次行汇编代码对应前面
(4)第三行汇编代码将变量n的数值放入寄存器eax中第四行汇编代码将其加一第五行汇编代码将结果放回变量n这里可以特别清晰地看到,EBP寄存港用于参数的寻址现在我们再看看B处函数调用的状况j=0xl2ff78ebp-8为j的地址004010C8pushecx004010C9call@TLT+20fn00401019可以看到,除了压栈数值变化以外,没有其它不同了A处压栈iB处压栈下面使用VC单步调试,再来看函数体内部,汇编代码没有任何不同,不同的只是栈当然了,函数体的代码是编译器一次性编译的即使被多次调用,去完成不同的工作,不同的只是参数调用栈,函数内部使用间接寻址,只是相同的机器码操作不同的内存存储区而已intfnintn{00401050pushebp00401051movebpespn+=l;00401068moveaxdwordptr[ebp+8]〃此时ebp=0x12ff20n=0xl2ff28eax=100040106Baddeax10040106Emovdwordptr[ebp+8]eax到了这里,栈的概念就讲完了
二、解析多线程这里首先要明确一点每一个线程都独立拥有一个栈我们知道,Windows系统是一个多任务操作系统,多个线程可以“同时”执行前面讲到,CPU执行程序代码完全依靠各种寄存器当一个线程将被挂起时,当前的各种寄存器的数值就被存储在了线程的栈中当CPU重新执行此线程时,将从栈中取出寄存器的数值,接着运行,似乎这个线程从来就没有被打断过一样正是由于每个线程都有一个独立的栈,使线程拥有了可以“闭门造车”的力量只要将参数传递给线程的栈,CPU将担负起这块内存存储区的管理工作,并适时地执行线程函数代码对其进行操作,全部这一切与前面所叙述的没有不同当系统在多个线程间切换时,CPU将执行相同的代码操作不同的栈下面举一个例子来深入理解随着面对对象编程方法的普及,我们很愿意将任何操作都包装成为一个类线程函数也不例外,以静态函数的形式将线程函数放在类中是C++编程普遍使用的i种方法通常状况下对象包括属性(类变量)与方法(类函数)属性指明对象自身的性质,方法用于操作对象,转变它的属性现在有一个小问题要留意了类的静态函数只能访问类的静态变量而静态变量是不属于单个对象的他存放在进程的全局数据存储区一般状况下,我们盼望每个对象能够“独立”,也就是说,多个对象能够各自干各自的工作,不要相互打搅假如以通常的方法以类(静态)变量存储对象的属性,可就要出问题了,由于类(静态)变量不属于单个对象现在怎么办呢?如何连续保持每个对象的“独立性”解决的方法就是使用栈,将参数传递给线程函数的局部变量(栈存储区),以单个对象管理每个线程,问题就解决了当然了,解决方法是多种多样的,这里只是为了进一步解释多线程与对象的关系由于Windows的内部实现实在是太简单了,这里只是在应用的层面上对栈给出解释若深化到Windows内部,栈的定位首先需要依据寄存器SS经由(全局或局部)段描述符表得到相应的线性地址(虚拟地址)基址,此基址与ETP相加,然后再经由分页机制寻址物理内存有爱好的读者可以参阅文章后面的参考书目一点学习体会写出来与大家共享,有不对的地方,欢迎指正的〃123456〃优化成一个地方}
二、堆和栈的理论学问1申请方式stack:由系统自动安排例如,声明在函数中一个局部变量intb;系统自动在栈中为b开拓空间heap:需要程序员自己申请,并指明大小,在c中malloc函数如pl=char*malloc10;在C++中用new运算符如p2=char*malloc10;但是留意pl、p2本身是在栈中的申请后系统的响应栈只要栈的剩余空间大于所申请空间,系统将为程序供应内存,否则将报特别提示栈溢出堆首先应当知道操作系统有一个纪录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,查找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间安排给程序,此外,对于大多数系统,会在这块内存空间中的首地址处纪录本次安排的大小,这样,代码中的delete语句才能正确的释放本内存空间此外,由于找到的堆结点的大小不肯定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中申请大小的限制栈在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M总之是一个编译时就确定的常数),假如申请的空间超过栈的剩余空间时,将提示overflow因此,能从栈获得的空间较小堆堆是向高地址扩展的数据结构,是不连续的内存区域这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址堆的大小受限于计算机系统中有效的虚拟内存由此可见,堆获得的空间比较敏捷,也比较大申请效率的比较栈由系统自动安排,速度较快但程序员是无法掌握的堆是由new安排的内存,一般速度比较慢,而且简洁产生内存碎片,不过用起来最便利.此外在WINDOWS下最好的方式是用VirtualAlloc安排内存他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不便利但是速度快,也最敏捷
2.5堆和栈中的存储内容栈在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量留意静态变量是不入栈的当本次函数调用结束后,局部变量先出栈,然后是参数,最终栈顶指针指向最开头存的地址,也就是主函数中的下一条指令,程序由该点连续运行堆一般是在堆的头部用一个字节存放堆的大小堆中的具体内容有程序员支配
2.6存取效率的比较charsi[]=aaaaaaaaaaaaaaa^;char*s2=bbbbbbbbbbbbbbbbb”;FCmovbyteptr[ebp-4]al第一种在读取时直接就把字符串中的元素读到寄存器cl中,而其次种则要先把指针值读到edx中,在依据edx读取字符,明显慢了
2.7小结堆和栈的区分可以用如下的比方来看出使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理睬切菜、洗菜等预备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小使用堆就象是自己动手做喜爱吃的菜肴,比较麻烦,但是比较符合自己的口味而且自由度大堆和栈的联系与区分dd在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我打算拿他第一个开刀首先,我们举一个例子voidf{int*p=newint
[5];}这条短短的一句话就包含了堆与栈,看到new我们首先就应当想到,我们安排了一块堆内存,那么指针p呢?他安排的是一块栈内存,所以这句话的意思就是在栈内存中存放了一个指向一块堆内存的指针P在程序会先确定在堆中安排内存的大小,然后调用operatornew安排内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下这里,我们为了简洁并没有释放内存,那么该怎么去释放呢?是deleteP么?澳,错了,应当是delete[]p这是为了告知编译器我删除的是一个数组,VC6就会依据相应的Cookie信息去进行释放内存的工作好了,我们回到我们的主题堆和栈毕竟有什么区分?主要的区分由以下几点
1、管理方式不同;
2、空间大小不同;
3、能否产生碎片不同;
4、生长方向不同;
5、安排方式不同;
6、安排效率不同;管理方式对于栈来讲,是由编译滞自动管理,无需我们手工掌握;对于堆来说,释放工作由程序员掌握,简洁产生memoryleako空间大小一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的但是对于栈来讲,一般都是有肯定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(似乎是,记不清晰了)当然,我们可以修改打开工程,依次操作菜单如下:Project-Setting-Link在Category中选中Output然后在Reserve中设定堆栈的最大值和commit留意reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开拓较大的值,可能增加内存的开销和启动时间碎片问题对于堆来讲,频繁的nc\v/dclctc势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低对于栈来讲,则不会存在这个问题由于栈是先进后出的队列,他们是如此的一一对应,以至于永久都不行能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容己经被弹出,具体的可以参考数据结构,这里我们就不再一一争论了生长方向对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长安排方式堆都是动态安排的,没有静态安排的堆栈有2种安排方式:静态安排和动态安排静态安排是编译器完成的,比如局部变量的安排动态安排由alloca函数进行安排,但是栈的动态安排和堆是不同的,他的动态安排是由编译器进行释放,无需我们手工实现安排效率栈是机潜系统供应的数据结构,计算机会在底层对栈供应支持安排特地的寄存器存放栈的地址,压栈出栈都有特地的指令执行,这就打算了栈的效率比较高堆则是C/C++函数库供应的,它的机制是很简单的,例如为了安排一块内存,库函数会依据肯定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜寻可用的足够大小的空间,假如没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回明显,堆的效率比栈要低得多从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,简洁造成大量的内存碎片;由于没有特地的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵所以栈在程序中是应用最广泛的,就算是函数的调用也采用栈去完成,函数调用过程中的参数,返回地址EBP和局部变量都采纳栈的方式存放所以,我们推举大家尽量用栈,而不是用堆虽然栈有如此众多的好处,但是由于和堆相比不是那么敏捷,有时候安排大量的内存空间,还是用堆好一些无论是堆还是栈,都要防止越界现象的发生(除非你是有意使其越界),由于越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要当心,说不定什么时候就崩掉,那时候debug可是相当困难的)对了,还有一件事,假如有人把堆栈合起来说,那它的意思是栈,可不是堆呵呵清晰了通过堆栈调用解析多线程首先说明一下,堆是进程的全局数据内存存储区,栈是函数的局部数据内存存储区由于大多数书籍在介绍堆或栈时,皆以堆栈泛指,因此,题目标题亦如此表述,盼望读者不要混淆就是了初见标题,或许有人觉得惊奇,多线程和堆栈有关系吗?初学多线程,许多概念难以辨清要全面深化理解多线程,必需对栈有特别清晰的理解个人感觉在Windows编程中,栈的概念如同C/C++中的指针,特别重要,但难于全面理解市面上的书籍对堆栈的介绍或是蜻蜓点水、浮于表面;或是过于理论化,不够具体,不易理解在这里,我以示例的形式将自己的一点学习体会写出来与大家共享为了便于表述清晰,文章分为两部分第一部分介绍栈的调用,这是此篇文章的核心其次部分解析多线程的概念
一、栈的调用众所周知,在函数调用过程中,参数的传递是通过栈完成的,具体到机器码是什么样子呢?不同的调用商定(PASCAL商定或STDCALL商定等)将导致不同的参数压栈挨次,这些细节就略去不讲了,有爱好的读者可以参考相关书目为了把栈的概念表述清晰,这里将涉及到一些简洁的汇编语言方面的学问,一点点而已然后以一个简洁的C++掌握台程序为示例来进一步具体说明先把代码列出来,够简洁吧#includeiostream.hintfn(intn)n+=l;returnn;voidmaininti=lj=10;i=fni;//Aj=fnj;//Bcout«i«,z,z«j«endl;现在我们需要一点汇编方面的学问,以更好地了解栈是个什么东东通俗一点讲,栈就是一块内存存储区,但是,这块内存存储区的操作使用有点特殊栈是由CPU直接管理的内存数组,CPU使用寄存器对其进行管理ESP寄存器存放栈底栈是向下增长的数据的地址push指令将数据压入栈中,ESP寄存冷的值将随之减小;相反,pop指令将数据从栈底弹出,ESP寄存器的值随之增大这样,ESP寄存器将始终存放栈底数据的地址另一方面呢,CPU在执行代码的时候,完全是依靠CPU内部的寄存器完成的通过EIP寄存器查找当前要执行的代码,通过EBP、ESP定位函数参数的地址、函数局部变量,等等等等下面让我们看一下,当某一个函数被调用时,栈将有何变动
1、函数参数被压入栈中
2、返回地址被调用函数执行完后将被执行的语句地址被压入栈中,函数被调用,此时,CPU将预备执行函数体内的代码
3、函数代码开头执行,首先执行的是将EBP压入栈中
4、使EBP的值等于ESP现在,EBP纪录的是当前栈底地址ESP以后函数就可以采用EBP对压栈的函数参数进行寻址了此时已压栈的有函数参数、返回地址、EBP原始值
5、从ESP中减掉肯定的数值,为函数留下局部变量空间此后,ESP将用于局部变量的寻址编译器设计者的聪慧才智真是令人鄙视高级语言书写简洁的一句函数00401028push14h0040102Acalloperatornew004010600040102Faddesp400401032movdwordptr[ebp_8]eax00401035moveaxdwordptr[ebp_8]00401038movdwordptr[ebp-4]eax。