p2c网站方案,北京 网站建设 知乎,有什么推荐做简历的网站,看视频的软件哪个最好免费详见#xff1a;http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytpo3 java程序的内存分配 JAVA 文件编译执行与虚拟机(JVM)介绍 Java 虚拟机(JVM)是可运行Java代码的假想计算机。只要根据JVM规格描述将解释器移植到特定的计算机上#xff0c;就能保证经过编译的任…详见http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytpo3 java程序的内存分配 JAVA 文件编译执行与虚拟机(JVM)介绍 Java 虚拟机(JVM)是可运行Java代码的假想计算机。只要根据JVM规格描述将解释器移植到特定的计算机上就能保证经过编译的任何Java代码能够在该系统上运行。本文首先简要介绍从Java文件的编译到最终执行的过程随后对JVM规格描述作一说明。 一.Java源文件的编译、下载、解释和执行 Java应用程序的开发周期包括编译、下载、解释和执行几个部分。Java编译程序将Java源程序翻译为JVM可执行代码?字节码。这一编译过程同C/C的编译有些不同。当C编译器编译生成一个对象的代码时该代码是为在某一特定硬件平台运行而产生的。因此在编译过程中编译程序通过查表将所有对符号的引用转换为特定的内存偏移量以保证程序运行。Java编译器却不将对变量和方法的引用编译为数值引用也不确定程序执行过程中的内存布局而是将这些符号引用信息保留在字节码中由解释器在运行过程中创立内存布局然后再通过查表来确定一个方法所在的地址。这样就有效的保证了Java的可移植性和安全性。 运行JVM字节码的工作是由解释器来完成的。解释执行过程分三部进行代码的装入、代码的校验和代码的执行。装入代码的工作由类装载器class loader完成。类装载器负责装入运行一个程序需要的所有代码这也包括程序代码中的类所继承的类和被其调用的类。当类装载器装入一个类时该类被放在自己的名字空间中。除了通过符号引用自己名字空间以外的类类之间没有其他办法可以影响其他类。在本台计算机上的所有类都在同一地址空间内而所有从外部引进的类都有一个自己独立的名字空间。这使得本地类通过共享相同的名字空间获得较高的运行效率同时又保证它们与从外部引进的类不会相互影响。当装入了运行程序需要的所有类后解释器便可确定整个可执行程序的内存布局。解释器为符号引用同特定的地址空间建立对应关系及查询表。通过在这一阶段确定代码的内存布局Java很好地解决了由超类改变而使子类崩溃的问题同时也防止了代码对地址的非法访问。 随后被装入的代码由字节码校验器进行检查。校验器可发现操作数栈溢出非法数据类型转化等多种错误。通过校验后代码便开始执行了。 Java字节码的执行有两种方式 1.即时编译方式解释器先将字节码编译成机器码然后再执行该机器码。 2.解释执行方式解释器通过每次解释并执行一小段代码来完成Java字节码程序的所有操作。类加载是具有惰性原则的在需要的时候才进行加载类的加载是以字节流的形式将字节码文件读入到内存中的 通常采用的是第二种方法。由于JVM规格描述具有足够的灵活性这使得将字节码翻译为机器代码的工作 具有较高的效率。对于那些对运行速度要求较高的应用程序解释器可将Java字节码即时编译为机器码从而很好地保证了Java代码的可移植性和高性能。 二.JVM规格描述 JVM的设计目标是提供一个基于抽象规格描述的计算机模型为解释程序开发人员提很好的灵活性同时也确保Java代码可在符合该规范的任何系统上运行。JVM对其实现的某些方面给出了具体的定义特别是对Java可执行代码即字节码(Bytecode)的格式给出了明确的规格。这一规格包括操作码和操作数的语法和数值、标识符的数值表示方式、以及Java类文件中的Java对象、常量缓冲池在JVM的存储映象。这些定义为JVM解释器开发人员提供了所需的信息和开发环境。Java的设计者希望给开发人员以随心所欲使用Java的自由。 JVM定义了控制Java代码解释执行和具体实现的五种规格它们是 JVM指令系统 JVM寄存器 JVM栈结构 JVM碎片回收堆 JVM存储区 2.1JVM指令系统 JVM指令系统同其他计算机的指令系统极其相似。Java指令也是由操作码和操作数两部分组成。操作码为8位二进制数操作数进紧随在操作码的后面其长度根据需要而不同。操作码用于指定一条指令操作的性质在这里我们采用汇编符号的形式进行说明如iload表示从存储器中装入一个整数anewarray表示为一个新数组分配空间iand表示两个整数的与ret用于流程控制表示从对某一方法的调用中返回。当长度大于8位时操作数被分为两个以上字节存放。JVM采用了big endian的编码方式来处理这种情况即高位bits存放在低字节中。这同 Motorola及其他的RISC CPU采用的编码方式是一致的而与Intel采用的little endian 的编码方式即低位bits存放在低位字节的方法不同。 Java指令系统是以Java语言的实现为目的设计的其中包含了用于调用方法和监视多先程系统的指令。Java的8位操作码的长度使得JVM最多有256种指令目前已使用了160多种操作码。 2.2JVM指令系统 所有的CPU均包含用于保存系统状态和处理器所需信息的寄存器组。如果虚拟机定义较多的寄存器便可以从中得到更多的信息而不必对栈或内存进行访问这有利于提高运行速度。然而如果虚拟机中的寄存器比实际CPU的寄存器多在实现虚拟机时就会占用处理器大量的时间来用常规存储器模拟寄存器这反而会降低虚拟机的效率。针对这种情况JVM只设置了4个最为常用的寄存器。它们是 pc程序计数器 optop操作数栈顶指针 frame当前执行环境指针 vars指向当前执行环境中第一个局部变量的指针 所有寄存器均为32位。pc用于记录程序的执行。optop,frame和vars用于记录指向Java栈区的指针。 2.3JVM栈结构 作为基于栈结构的计算机Java栈是JVM存储信息的主要方法。当JVM得到一个Java字节码应用程序后便为该代码中一个类的每一个方法创建一个栈框架以保存该方法的状态信息。每个栈框架包括以下三类信息 局部变量 执行环境 操作数栈 局部变量用于存储一个类的方法中所用到的局部变量。vars寄存器指向该变量表中的第一个局部变量。 执行环境用于保存解释器对Java字节码进行解释过程中所需的信息。它们是上次调用的方法、局部变量指针和操作数栈的栈顶和栈底指针。执行环境是一个执行一个方法的控制中心。例如如果解释器要执行iadd(整数加法)首先要从frame寄存器中找到当前执行环境而后便从执行环境中找到操作数栈从栈顶弹出两个整数进行加法运算最后将结果压入栈顶。 操作数栈用于存储运算所需操作数及运算的结果。 2.4JVM碎片回收堆 Java类的实例所需的存储空间是在堆上分配的。解释器具体承担为类实例分配空间的工作。解释器在为一个实例分配完存储空间后便开始记录对该实例所占用的内存区域的使用。一旦对象使用完毕便将其回收到堆中。 在Java语言中除了new语句外没有其他方法为一对象申请和释放内存。对内存进行释放和回收的工作是由Java运行系统承担的。这允许Java运行系统的设计者自己决定碎片回收的方法。在SUN公司开发的Java解释器和Hot Java环境中碎片回收用后台线程的方式来执行。这不但为运行系统提供了良好的性能而且使程序设计人员摆脱了自己控制内存使用的风险。 2.5JVM存储区 JVM有两类存储区常量缓冲池和方法区。常量缓冲池用于存储类名称、方法和字段名称以及串常量。方法区则用于存储Java方法的字节码。对于这两种存储区域具体实现方式在JVM规格中没有明确规定。这使得Java应用程序的存储布局必须在运行过程中确定依赖于具体平台的实现方式。 JVM是为Java字节码定义的一种独立于具体平台的规格描述是Java平台独立性的基础。目前的JVM还存在一些限制和不足有待于进一步的完善但无论如何JVM的思想是成功的。 对比分析如果把Java原程序想象成我们的C原程序Java原程序编译后生成的字节码就相当于C原程序编译后的80x86的机器码二进制程序文件JVM虚拟机相当于80x86计算机系统,Java解释器相当于80x86CPU。在80x86CPU上运行的是机器码在Java解释器上运行的是Java字节码。 Java解释器相当于运行Java字节码的“CPU”,但该“CPU”不是通过硬件实现的而是用软件实现的。Java解释器实际上就是特定的平台下的一个应用程序。只要实现了特定平台下的解释器程序Java字节码就能通过解释器程序在该平台下运行这是Java跨平台的根本。当前并不是在所有的平台下都有相应Java解释器程序这也是Java并不能在所有的平台下都能运行的原因它只能在已实现了Java解释器程序的平台下运行。 堆和栈的区别 非本人作也!因非常经典,所以收归旗下,与众人阅之!原作者不祥! 堆和栈的区别 一、预备知识—程序的内存分配 一个由c/C编译的程序占用的内存分为以下几个部分 1、栈区stack— 由编译器自动分配释放 存放函数的参数值局部变量的值等。其操作方式类似于数据结构中的栈。 2、堆区heap — 一般由程序员分配释放 若程序员不释放程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事分配方式倒是类似于链表呵呵。 3、全局区静态区static—全局变量和静态变量的存储是放在一块的初始化的全局变量和静态变量在一块区域 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放 5、程序代码区—存放函数体的二进制代码。 二、例子程序 这是一个前辈写的非常详细 //main.cpp int a 0; 全局初始化区 char *p1; 全局未初始化区 main() { int b; 栈 char s[] abc; 栈 char *p2; 栈 char *p3 123456; 123456\0在常量区p3在栈上。 static int c 0 全局静态初始化区 p1 (char *)malloc(10); p2 (char *)malloc(20); 分配得来得10和20字节的区域就在堆区。 strcpy(p1, 123456); 123456\0放在常量区编译器可能会将它与p3所指向的123456优化成一个地方。 } 二、堆和栈的理论知识 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 [edx1] 00401073 88 45 FC mov byte ptr [ebp-4],al 第一种在读取时直接就把字符串中的元素读到寄存器cl中而第二种则要先把指针值读到edx中在根据edx读取字符显然慢了。 2.7 小结 堆和栈的区别可以用如下的比喻来看出 使用栈就象我们去饭馆里吃饭只管点菜发出申请、付钱、和吃使用吃饱了就走不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作他的好处是快捷但是自由度小。 使用堆就象是自己动手做喜欢吃的菜肴比较麻烦但是比较符合自己的口味而且自由度大。 windows 进程中的内存结构 在阅读本文之前如果你连堆栈是什么多不知道的话请先阅读文章后面的基础知识。 接触过编程的人都知道高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢程序又是如何使用这些变量的呢下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明默认都使用VC编译的release版。 首先来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local)静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码 #include int g10, g20, g30; int main() { static int s10, s20, s30; int v10, v20, v30; // 打印出各个变量的内存地址 printf(0x%08x\n,v1); // 打印各本地变量的内存地址 printf(0x%08x\n,v2); printf(0x%08x\n\n,v3); printf(0x%08x\n,g1); //打印各全局变量的内存地址 printf(0x%08x\n,g2); printf(0x%08x\n\n,g3); printf(0x%08x\n,s1); //打印各静态变量的内存地址 printf(0x%08x\n,s2); printf(0x%08x\n\n,s3); return 0; } 编译后的执行结果是 0x0012ff78 0x0012ff7c 0x0012ff80 0x004068d0 0x004068d4 0x004068d8 0x004068dc 0x004068e0 0x004068e4 输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量g1,g2,g3是全局变量s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的但是本地变量和全局变量分配的内存地址差了十万八千里而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言可以在逻辑上分成3个部份代码区静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区栈是一种线性结构堆是一种链式结构。进程的每个线程都有私有的“栈”所以每个线程虽然代码一样但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区本地变量分配在动态数据区即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。 ├———————┤ 低端内存区域 │ …… │ ├———————┤ │ 动态数据区 │ ├———————┤ │ …… │ ├———————┤ │ 代码区 │ ├———————┤ │ 静态数据区 │ ├———————┤ │ …… │ ├———————┤高端内存区域 堆栈是一个先进后出的数据结构栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的前者由被调函数调整堆栈后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码 #include void __stdcall func(int param1,int param2,int param3) { int var1param1; int var2param2; int var3param3; printf(0x%08x\n,¶m1); // 打印出各个变量的内存地址 printf(0x%08x\n,¶m2); printf(0x%08x\n\n,¶m3); printf(0x%08x\n,var1); printf(0x%08x\n,var2); printf(0x%08x\n\n,var3); return; } int main() { func(1,2,3); return 0; } 编译后的执行结果是 0x0012ff78 0x0012ff7c 0x0012ff80 0x0012ff68 0x0012ff6c 0x0012ff70 ├———————┤— 函数执行时的栈顶ESP、低端内存区域 │ …… │ ├———————┤ │ var 1 │ ├———————┤ │ var 2 │ ├———————┤ │ var 3 │ ├———————┤ │ RET │ ├———————┤—“__cdecl”函数返回后的栈顶ESP │ parameter 1 │ ├———————┤ │ parameter 2 │ ├———————┤ │ parameter 3 │ ├———————┤—“__stdcall”函数返回后的栈顶ESP │ …… │ ├———————┤—栈底基地址 EBP、高端内存区域 上图就是函数调用过程中堆栈的样子了。首先三个参数以从又到左的次序压入堆栈先压“param3”再压“param2”最后压入“param1”然后压入函数的返回地址(RET)接着跳转到函数地址接着执行这里要补充一点介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后继续压入当前EBP然后用当前ESP代替EBP。然而有一篇介绍windows下函数调用的文章中说在windows下的函数调用也有这一步骤但根据我的实际调试并未发现这一步这还可以从param3和var1之间只有4字节的间隙这点看出来第三步将栈顶(ESP)减去一个数为本地变量分配内存空间上例中是减去12字节(ESPESP-3*4每个int变量占用4个字节)接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈所以在函数返回前要恢复堆栈先回收本地变量占用的内存(ESPESP3*4)然后取出返回地址填入EIP寄存器回收先前压入参数占用的内存(ESPESP3*4)继续执行调用者的代码。参见下列汇编代码 ;--------------func 函数的汇编代码------------------- :00401000 83EC0C sub esp, 0000000C // 创建本地变量的内存空间 :00401003 8B442410 mov eax, dword ptr [esp10] :00401007 8B4C2414 mov ecx, dword ptr [esp14] :0040100B 8B542418 mov edx, dword ptr [esp18] :0040100F 89442400 mov dword ptr [esp], eax :00401013 8D442410 lea eax, dword ptr [esp10] :00401017 894C2404 mov dword ptr [esp04], ecx …………………… 省略若干代码 :00401075 83C43C add esp, 0000003C ; 恢复堆栈回收本地变量的内存空间 :00401078 C3 ret 000C ;函数返回恢复参数占用的内存空间 ;如果是“__cdecl”的话这里是“ret”堆栈将由调用者恢复 ;------------------- 函数结束------------------------- ;-------------- 主程序调用func函数的代码-------------- :00401080 6A03 push 00000003 // 压入参数param3 :00401082 6A02 push 00000002 //压入参数param2 :00401084 6A01 push 00000001 //压入参数param1 :00401086 E875FFFFFF call 00401000 //调用func函数 ;如果是“__cdecl”的话将在这里恢复堆栈“add esp, 0000000C” 聪明的读者看到这里差不多就明白缓冲溢出的原理了。先来看下面的代码 #include #include void __stdcall func() { char lpBuff[8]\0; strcat(lpBuff,AAAAAAAAAAA); return; } int main() { func(); return 0; } 编译后执行一下回怎么样哈“0x00414141指令引用的0x00000000内存。该内存不能为read。”“非法操作”喽41就是A的16进制的ASCII码了那明显就是strcat这句出的问题了。lpBuff的大小只有8字节算进结尾的\0那strcat最多只能写入7个A但程序实际写入了11个A外加1个\0。再来看看上面那幅图多出来的4个字节正好覆盖了RET的所在的内存空间导致函数返回到一个错误的内存地址执行了错误的指令。如果能精心构造这个字符串使它分成三部分前一部份仅仅是填充的无意义数据以达到溢出的目的接着是一个覆盖RET的数据紧接着是一段shellcode那只要着个RET地址能指向这段shellcode的第一个指令那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令使得exploit有更强的通用性。 ├———————┤— 低端内存区域 │ …… │ ├———————┤—由exploit填入数据的开始 │ │ │ buffer │—填入无用的数据 │ │ ├———————┤ │ RET │—指向shellcode或NOP指令的范围 ├———————┤ │ NOP │ │ …… │—填入的NOP指令是RET可指向的范围 │ NOP │ ├———————┤ │ │ │ shellcode │ │ │ ├———————┤—由exploit填入数据的结束 │ …… │ ├———————┤—高端内存区域 windows 下的动态数据除了可存放在栈中还可以存放在堆中。了解C的朋友都知道C可以使用new关键字来动态分配内存。来看下面的C代码 #include #include #include void func() { char *buffernew char[128]; char bufflocal[128]; static char buffstatic[128]; printf(0x%08x\n,buffer); // 打印堆中变量的内存地址 printf(0x%08x\n,bufflocal); //打印本地变量的内存地址 printf(0x%08x\n,buffstatic); //打印静态变量的内存地址 } void main() { func(); return; } 程序执行结果为 0x004107d0 0x0012ff04 0x004068c0 可以发现用new关键字分配的内存即不在栈中也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前先来了解一下和“堆”有关的几个API函数 HeapAlloc 在堆中申请内存空间 HeapCreate 创建一个新的堆对象 HeapDestroy 销毁一个堆对象 HeapFree 释放申请的内存 HeapWalk 枚举堆对象的所有内存块 GetProcessHeap 取得进程的默认堆对象 GetProcessHeaps 取得进程所有的堆对象 LocalAlloc GlobalAlloc 当进程初始化时系统会自动为进程创建一个默认堆这个堆默认所占内存的大小为1M。堆对象由系统进行管理它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间 HANDLE hHeapGetProcessHeap(); char *buffHeapAlloc(hHeap,0,8); 其中hHeap是堆对象的句柄buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢它的值有什么意义吗看看下面这段代码吧 #pragma comment(linker,/entry:main) // 定义程序的入口 #include _CRTIMP int (__cdecl *printf)(const char *, ...); // 定义STL函数printf /*--------------------------------------------------------------------------- 写到这里我们顺便来复习一下前面所讲的知识 (*注)printf函数是C语言的标准函数库中函数VC的标准函数库由msvcrt.dll模块实现。 由函数定义可见printf的参数个数是可变的函数内部无法预先知道调用者压入的参数个数函数只能通过分析第一个参数字符串的格式来获得压入参数的信息由于这里参数的个数是动态的所以必须由调用者来平衡堆栈这里便使用了__cdecl调用规则。BTWWindows系统的API函数基本上是__stdcall调用形式只有一个API例外那就是wsprintf它使用__cdecl调用规则同printf函数一样这是由于它的参数个数是可变的缘故。 ---------------------------------------------------------------------------*/ void main() { HANDLE hHeapGetProcessHeap(); char *buffHeapAlloc(hHeap,0,0x10); char *buff2HeapAlloc(hHeap,0,0x10); HMODULE hMsvcrtLoadLibrary(msvcrt.dll); printf(void *)GetProcAddress(hMsvcrt,printf); printf(0x%08x\n,hHeap); printf(0x%08x\n,buff); printf(0x%08x\n\n,buff2); } 执行结果为 0x00130000 0x00133100 0x00133118 hHeap 的值怎么和那个buff的值那么接近呢其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构这个结构中存放着一些有关进程的重要信息其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据如windows 2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的同一时刻只能有一个线程访问堆中的数据当多个线程同时有访问要求时只能排队等待这样便造成程序执行效率下降。 最后来说说内存中的数据对齐。所位数据对齐是指数据所在的内存地址必须是该数据长度的整数倍DWORD数据的内存起始地址能被4除尽WORD数据的内存起始地址能被2除尽x86 CPU能直接访问对齐的数据当他试图访问一个未对齐的数据时会在内部进行一系列的调整这些调整对于程序来说是透明的但是会降低运行速度所以编译器在编译程序时会尽量保证数据对齐。同样一段代码我们来看看用VC、Dev-C和lcc三个不同编译器编译出来的程序的执行结果 #include int main() { int a; char b; int c; printf(0x%08x\n,a); printf(0x%08x\n,b); printf(0x%08x\n,c); return 0; } 这是用VC编译后的执行结果 0x0012ff7c 0x0012ff7b 0x0012ff80 变量在内存中的顺序b(1字节)-a(4字节)-c(4字节)。 这是用Dev-C编译后的执行结果 0x0022ff7c 0x0022ff7b 0x0022ff74 变量在内存中的顺序c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)。 这是用lcc编译后的执行结果 0x0012ff6c 0x0012ff6b 0x0012ff64 变量在内存中的顺序同上。 三个编译器都做到了数据对齐但是后两个编译器显然没VC“聪明”让一个char占了4字节浪费内存哦。 基础知识 堆栈是一种简单的数据结构是一种只允许在其一端进行插入或删除的线性表。允许插入或删除操作的一端称为栈顶另一端称为栈底对堆栈的插入和删除操作被称为入栈和出栈。有一组CPU指令可以实现对进程的内存实现堆栈访问。其中POP指令实现出栈操作PUSH指令实现入栈操作。CPU的ESP寄存器存放当前线程的栈顶指针EBP寄存器中保存当前线程的栈底指针。CPU的EIP寄存器存放下一个CPU指令存放的内存地址当CPU执行完当前的指令后从EIP寄存器中读取下一条指令的内存地址然后继续执行。 参考《Windows下的HEAP溢出及其利用》by: isno 《windows核心编程》by: Jeffrey Richter 摘要 讨论常见的堆性能问题以及如何防范它们。共 9 页 前言 您是否是动态分配的 C/C 对象忠实且幸运的用户您是否在模块间的往返通信中频繁地使用了“自动化”您的程序是否因堆分配而运行起来很慢不仅仅您遇到这样的问题。几乎所有项目迟早都会遇到堆问题。大家都想说“我的代码真正好只是堆太慢”。那只是部分正确。更深入理解堆及其用法、以及会发生什么问题是很有用的。 什么是堆 如果您已经知道什么是堆可以跳到“什么是常见的堆性能问题”部分 在程序中使用堆来动态分配和释放对象。在下列情况下调用堆操作 事先不知道程序所需对象的数量和大小。 对象太大而不适合堆栈分配程序。 堆使用了在运行时分配给代码和堆栈的内存之外的部分内存。下图给出了堆分配程序的不同层。 nloadjavascript:if(this.widthscreen.width-333)this.widthscreen.width-333 border0 dypop按此在新窗口浏览图片 GlobalAlloc/GlobalFree Microsoft Win32 堆调用这些调用直接与每个进程的默认堆进行对话。 LocalAlloc/LocalFree Win32 堆调用为了与 Microsoft Windows NT 兼容这些调用直接与每个进程的默认堆进行对话。 COM 的 IMalloc 分配程序或 CoTaskMemAlloc / CoTaskMemFree函数使用每个进程的默认堆。自动化程序使用“组件对象模型 (COM)”的分配程序而申请的程序使用每个进程堆。 C/C 运行时 (CRT) 分配程序提供了 malloc() 和 free() 以及 new 和 delete 操作符。如 Microsoft Visual Basic 和 Java 等语言也提供了新的操作符并使用垃圾收集来代替堆。CRT 创建自己的私有堆驻留在 Win32 堆的顶部。 Windows NT 中Win32 堆是 Windows NT 运行时分配程序周围的薄层。所有 API 转发它们的请求给 NTDLL。 Windows NT 运行时分配程序提供 Windows NT 内的核心堆分配程序。它由具有 128 个大小从 8 到 1,024 字节的空闲列表的前端分配程序组成。后端分配程序使用虚拟内存来保留和提交页。 在图表的底部是“虚拟内存分配程序”操作系统使用它来保留和提交页。所有分配程序使用虚拟内存进行数据的存取。 分配和释放块不就那么简单吗为何花费这么长时间 堆实现的注意事项 传统上操作系统和运行时库是与堆的实现共存的。在一个进程的开始操作系统创建一个默认堆叫做“进程堆”。如果没有其他堆可使用则块的分配使用“进程堆”。语言运行时也能在进程内创建单独的堆。例如C 运行时创建它自己的堆。除这些专用的堆外应用程序或许多已载入的动态链接库 (DLL) 之一可以创建和使用单独的堆。Win32 提供一整套 API 来创建和使用私有堆。有关堆函数英文的详尽指导请参见 MSDN。 当应用程序或 DLL 创建私有堆时这些堆存在于进程空间并且在进程内是可访问的。从给定堆分配的数据将在同一个堆上释放。不能从一个堆分配而在另一个堆释放。 在所有虚拟内存系统中堆驻留在操作系统的“虚拟内存管理器”的顶部。语言运行时堆也驻留在虚拟内存顶部。某些情况下这些堆是操作系统堆中的层而语言运行时堆则通过大块的分配来执行自己的内存管理。不使用操作系统堆而使用虚拟内存函数更利于堆的分配和块的使用。 典型的堆实现由前、后端分配程序组成。前端分配程序维持固定大小块的空闲列表。对于一次分配调用堆尝试从前端列表找到一个自由块。如果失败堆被迫从后端保留和提交虚拟内存分配一个大块来满足请求。通用的实现有每块分配的开销这将耗费执行周期也减少了可使用的存储空间。 Knowledge Base 文章 Q10758“用 calloc() 和 malloc() 管理内存” 搜索文章编号, 包含了有关这些主题的更多背景知识。另外有关堆实现和设计的详细讨论也可在下列著作中找到“Dynamic Storage Allocation: A Survey and Critical Review”作者 Paul R. Wilson、Mark S. Johnstone、Michael Neely 和 David Boles“International Workshop on Memory Management”, 作者 Kinross, Scotland, UK, 1995 年 9 月(http://www.cs.utexas.edu/users/oops/papers.html)英文。 Windows NT 的实现Windows NT 版本 4.0 和更新版本 使用了 127 个大小从 8 到 1,024 字节的 8 字节对齐块空闲列表和一个“大块”列表。“大块”列表空闲列表[0] 保存大于 1,024 字节的块。空闲列表容纳了用双向链表链接在一起的对象。默认情况下“进程堆”执行收集操作。收集是将相邻空闲块合并成一个大块的操作。收集耗费了额外的周期但减少了堆块的内部碎片。 单一全局锁保护堆防止多线程式的使用。请参见“Server Performance and Scalability Killers”中的第一个注意事项, George Reilly 所著在 “MSDN Online Web Workshop”上站点http://msdn.microsoft.com/workshop/server/iis/tencom.asp英文。单一全局锁本质上是用来保护堆数据结构防止跨多线程的随机存取。若堆操作太频繁单一全局锁会对性能有不利的影响。 什么是常见的堆性能问题 以下是您使用堆时会遇到的最常见问题 分配操作造成的速度减慢。光分配就耗费很长时间。最可能导致运行速度减慢原因是空闲列表没有块所以运行时分配程序代码会耗费周期寻找较大的空闲块或从后端分配程序分配新块。 释放操作造成的速度减慢。释放操作耗费较多周期主要是启用了收集操作。收集期间每个释放操作“查找”它的相邻块取出它们并构造成较大块然后再把此较大块插入空闲列表。在查找期间内存可能会随机碰到从而导致高速缓存不能命中性能降低。 堆竞争造成的速度减慢。当两个或多个线程同时访问数据而且一个线程继续进行之前必须等待另一个线程完成时就发生竞争。竞争总是导致麻烦这也是目前多处理器系统遇到的最大问题。当大量使用内存块的应用程序或 DLL 以多线程方式运行或运行于多处理器系统上时将导致速度减慢。单一锁定的使用—常用的解决方案—意味着使用堆的所有操作是序列化的。当等待锁定时序列化会引起线程切换上下文。可以想象交叉路口闪烁的红灯处走走停停导致的速度减慢。 竞争通常会导致线程和进程的上下文切换。上下文切换的开销是很大的但开销更大的是数据从处理器高速缓存中丢失以及后来线程复活时的数据重建。 堆破坏造成的速度减慢。造成堆破坏的原因是应用程序对堆块的不正确使用。通常情形包括释放已释放的堆块或使用已释放的堆块以及块的越界重写等明显问题。破坏不在本文讨论范围之内。有关内存重写和泄漏等其他细节请参见 Microsoft Visual C(R) 调试文档 。 频繁的分配和重分配造成的速度减慢。这是使用脚本语言时非常普遍的现象。如字符串被反复分配随重分配增长和释放。不要这样做如果可能尽量分配大字符串和使用缓冲区。另一种方法就是尽量少用连接操作。 竞争是在分配和释放操作中导致速度减慢的问题。理想情况下希望使用没有竞争和快速分配/释放的堆。可惜现在还没有这样的通用堆也许将来会有。 在所有的服务器系统中如 IIS、MSProxy、DatabaseStacks、网络服务器、 Exchange 和其他, 堆锁定实在是个大瓶颈。处理器数越多竞争就越会恶化。 尽量减少堆的使用 现在您明白使用堆时存在的问题了难道您不想拥有能解决这些问题的超级魔棒吗我可希望有。但没有魔法能使堆运行加快—因此不要期望在产品出货之前的最后一星期能够大为改观。如果提前规划堆策略情况将会大大好转。调整使用堆的方法减少对堆的操作是提高性能的良方。 如何减少使用堆操作通过利用数据结构内的位置可减少堆操作的次数。请考虑下列实例 struct ObjectA { // objectA 的数据 } struct ObjectB { // objectB 的数据 } // 同时使用 objectA 和 objectB // // 使用指针 // struct ObjectB { struct ObjectA * pObjA; // objectB 的数据 } // // 使用嵌入 // struct ObjectB { struct ObjectA pObjA; // objectB 的数据 } // // 集合 – 在另一对象内使用 objectA 和 objectB // struct ObjectX { struct ObjectA objA; struct ObjectB objB; } 避免使用指针关联两个数据结构。如果使用指针关联两个数据结构前面实例中的对象 A 和 B 将被分别分配和释放。这会增加额外开销—我们要避免这种做法。 把带指针的子对象嵌入父对象。当对象中有指针时则意味着对象中有动态元素百分之八十和没有引用的新位置。嵌入增加了位置从而减少了进一步分配/释放的需求。这将提高应用程序的性能。 合并小对象形成大对象聚合。聚合减少分配和释放的块的数量。如果有几个开发者各自开发设计的不同部分则最终会有许多小对象需要合并。集成的挑战就是要找到正确的聚合边界。 内联缓冲区能够满足百分之八十的需要aka 80-20 规则。个别情况下需要内存缓冲区来保存字符串/二进制数据但事先不知道总字节数。估计并内联一个大小能满足百分之八十需要的缓冲区。对剩余的百分之二十可以分配一个新的缓冲区和指向这个缓冲区的指针。这样就减少分配和释放调用并增加数据的位置空间从根本上提高代码的性能。 在块中分配对象块化。块化是以组的方式一次分配多个对象的方法。如果对列表的项连续跟踪例如对一个 {名称值} 对的列表有两种选择选择一是为每一个“名称-值”对分配一个节点选择二是分配一个能容纳如五个“名称-值”对的结构。例如一般情况下如果存储四对就可减少节点的数量如果需要额外的空间数量则使用附加的链表指针。 块化是友好的处理器高速缓存特别是对于 L1-高速缓存因为它提供了增加的位置 —不用说对于块分配很多数据块会在同一个虚拟页中。 正确使用 _amblksiz。C 运行时 (CRT) 有它的自定义前端分配程序该分配程序从后端Win32 堆分配大小为 _amblksiz 的块。将 _amblksiz 设置为较高的值能潜在地减少对后端的调用次数。这只对广泛使用 CRT 的程序适用。 使用上述技术将获得的好处会因对象类型、大小及工作量而有所不同。但总能在性能和可升缩性方面有所收获。另一方面代码会有点特殊但如果经过深思熟虑代码还是很容易管理的。 其他提高性能的技术 下面是一些提高速度的技术 使用 Windows NT5 堆 由于几个同事的努力和辛勤工作1998 年初 Microsoft Windows(R) 2000 中有了几个重大改进 改进了堆代码内的锁定。堆代码对每堆一个锁。全局锁保护堆数据结构防止多线程式的使用。但不幸的是在高通信量的情况下堆仍受困于全局锁导致高竞争和低性能。Windows 2000 中锁内代码的临界区将竞争的可能性减到最小,从而提高了可伸缩性。 使用 “Lookaside”列表。堆数据结构对块的所有空闲项使用了大小在 8 到 1,024 字节以 8-字节递增的快速高速缓存。快速高速缓存最初保护在全局锁内。现在使用 lookaside 列表来访问这些快速高速缓存空闲列表。这些列表不要求锁定而是使用 64 位的互锁操作因此提高了性能。 内部数据结构算法也得到改进。 这些改进避免了对分配高速缓存的需求但不排除其他的优化。使用 Windows NT5 堆评估您的代码它对小于 1,024 字节 (1 KB) 的块来自前端分配程序的块是最佳的。GlobalAlloc() 和 LocalAlloc() 建立在同一堆上是存取每个进程堆的通用机制。如果希望获得高的局部性能则使用 Heap(R) API 来存取每个进程堆或为分配操作创建自己的堆。如果需要对大块操作也可以直接使用 VirtualAlloc() / VirtualFree() 操作。 上述改进已在 Windows 2000 beta 2 和 Windows NT 4.0 SP4 中使用。改进后堆锁的竞争率显著降低。这使所有 Win32 堆的直接用户受益。CRT 堆建立于 Win32 堆的顶部但它使用自己的小块堆因而不能从 Windows NT 改进中受益。Visual C 版本 6.0 也有改进的堆分配程序。 使用分配高速缓存 分配高速缓存允许高速缓存分配的块以便将来重用。这能够减少对进程堆或全局堆的分配/释放调用的次数也允许最大限度的重用曾经分配的块。另外分配高速缓存允许收集统计信息,以便较好地理解对象在较高层次上的使用。 典型地自定义堆分配程序在进程堆的顶部实现。自定义堆分配程序与系统堆的行为很相似。主要的差别是它在进程堆的顶部为分配的对象提供高速缓存。高速缓存设计成一套固定大小如 32 字节、64 字节、128 字节等。这一个很好的策略但这种自定义堆分配程序丢失与分配和释放的对象相关的“语义信息”。 与自定义堆分配程序相反“分配高速缓存”作为每类分配高速缓存来实现。除能够提供自定义堆分配程序的所有好处之外它们还能够保留大量语义信息。每个分配高速缓存处理程序与一个目标二进制对象关联。它能够使用一套参数进行初始化这些参数表示并发级别、对象大小和保持在空闲列表中的元素的数量等。分配高速缓存处理程序对象维持自己的私有空闲实体池不超过指定的阀值并使用私有保护锁。合在一起分配高速缓存和私有锁减少了与主系统堆的通信量因而提供了增加的并发、最大限度的重用和较高的可伸缩性。 需要使用清理程序来定期检查所有分配高速缓存处理程序的活动情况并回收未用的资源。如果发现没有活动将释放分配对象的池从而提高性能。 可以审核每个分配/释放活动。第一级信息包括对象、分配和释放调用的总数。通过查看它们的统计信息可以得出各个对象之间的语义关系。利用以上介绍的许多技术之一这种关系可以用来减少内存分配。 分配高速缓存也起到了调试助手的作用帮助您跟踪没有完全清除的对象数量。通过查看动态堆栈返回踪迹和除没有清除的对象之外的签名甚至能够找到确切的失败的调用者。 MP 堆 MP 堆是对多处理器友好的分布式分配的程序包在 Win32 SDKWindows NT 4.0 和更新版本中可以得到。最初由 JVert 实现此处堆抽象建立在 Win32 堆程序包的顶部。MP 堆创建多个 Win32 堆并试图将分配调用分布到不同堆以减少在所有单一锁上的竞争。 本程序包是好的步骤 —一种改进的 MP-友好的自定义堆分配程序。但是它不提供语义信息和缺乏统计功能。通常将 MP 堆作为 SDK 库来使用。如果使用这个 SDK 创建可重用组件您将大大受益。但是如果在每个 DLL 中建立这个 SDK 库将增加工作设置。 重新思考算法和数据结构 要在多处理器机器上伸缩则算法、实现、数据结构和硬件必须动态伸缩。请看最经常分配和释放的数据结构。试问“我能用不同的数据结构完成此工作吗”例如如果在应用程序初始化时加载了只读项的列表这个列表不必是线性链接的列表。如果是动态分配的数组就非常好。动态分配的数组将减少内存中的堆块和碎片从而增强性能。 减少需要的小对象的数量减少堆分配程序的负载。例如我们在服务器的关键处理路径上使用五个不同的对象每个对象单独分配和释放。一起高速缓存这些对象把堆调用从五个减少到一个显著减少了堆的负载特别当每秒钟处理 1,000 个以上的请求时。 如果大量使用“Automation”结构请考虑从主线代码中删除“Automation BSTR”或至少避免重复的 BSTR 操作。BSTR 连接导致过多的重分配和分配/释放操作。 摘要 对所有平台往往都存在堆实现因此有巨大的开销。每个单独代码都有特定的要求但设计能采用本文讨论的基本理论来减少堆之间的相互作用。 评价您的代码中堆的使用。 改进您的代码以使用较少的堆调用分析关键路径和固定数据结构。 在实现自定义的包装程序之前使用量化堆调用成本的方法。 如果对性能不满意请要求 OS 组改进堆。更多这类请求意味着对改进堆的更多关注。 要求 C 运行时组针对 OS 所提供的堆制作小巧的分配包装程序。随着 OS 堆的改进C 运行时堆调用的成本将减小。 操作系统Windows NT 家族正在不断改进堆。请随时关注和利用这些改进。 Murali Krishnan 是 Internet Information Server (IIS) 组的首席软件设计工程师。从 1.0 版本开始他就设计 IIS并成功发行了 1.0 版本到 4.0 版本。Murali 组织并领导 IIS 性能组三年 (1995-1998), 从一开始就影响 IIS 性能。他拥有威斯康星州 Madison 大学的 M.S.和印度 Anna 大学的 B.S.。工作之外他喜欢阅读、打排球和家庭烹饪。 转载于:https://www.cnblogs.com/grefr/p/5046364.html