当用户启动一个程序时,系统会将程序文件从外部存储器(硬盘等)加载到内存中。当程序工作时,需要使用内存空间来放置代码和数据。在使用一段内存之前,程序需要以某种方式(API或库函数)发出申请,接受到申请的一方(内存管理器或C运行库)根据申请者的要求从可用(空闲)空间中寻找满足要求的内存区域分配给申请者。当程序不再需要该空间时应该通过与申请方式相对应的方法归还该空间,即释放。
在大型软件项目中,合理分配和释放内存是非常重要的,由于内存使用不当而导致的各种问题经常成为软件项目的严重阻碍。提高内存的使用率,降低内存分配和释放过程的复杂性一直是软件产业中的永恒话题。JAVA和.net语言的一个共同优势就是可以自动回收不再需要的内存,使程序员可以不用编写释放内存的代码。通过栈来分配局部变量也可以看做是简化内存使用的一种方法。
堆是组织内存的另外一种重要方法,是程序在运行期动态申请内存空间的主要途径。与栈空间是由编译器产生的代码自动分配和释放不同,堆上的空间需要程序员自己编写代码来申请(HeapAlloc)和释放(HeapFree),而且分配和释放操作应该严格匹配,忘记释放或多次释放都是不正确的。
与栈上的缓冲区溢出类似,如果向堆上的缓冲区写入超过其大小的内容,也会因为溢出而破坏堆上其他内容,可能导致严重的问题,包括程序崩溃。
为了帮助发现堆使用方面的问题,堆管理器,编译器和软件类库提供了很多检查和辅助调试机制。比如,Win32支持参数检查,溢出检查及释放检查等功能。VC编译器设计了专门的调试堆并提供了一系列用来追踪和检查堆使用情况的函数,在编译调试版本的可执行文件时,可以使用这些调试支持来解决内存泄漏等问题。
1.理解堆
栈是分配局部变量和存储函数调用参数以及返回位置的主要场所,系统在创建每个线程时会自动为其创建栈。对于C/C++这样的编程语言,编译器在编译阶段会生成合适的代码来从栈上分配和释放空间,不需要程序员编写任何额外的代码,出于这个原因栈得到了“自动内存”的一个美名。
不过从栈上分配内存也有不足之处,首先,栈空间(尤其是内核态栈)的容量是相对较小的,为了防止栈溢出,不适合在栈上分配特别大的内存区。其次,由于栈帧通常是随着函数的调用和返回而创建和消除的,因此分配在栈上的变量只是在函数内有效,这使栈只能分配局部变量,不适合分配需要较长生存期的全局变量和对象。第三,尽管也可以使用_alloca()这样的函数来从栈上分配可变长度的缓冲区,但是这样做回给异常处理(EH)带来麻烦,因此栈也不合适分配运行期才能决定大小(动态大小)的缓冲区。
从操作系统的角度看,堆是系统的内存管理功能向应用软件提供服务的一种方式。通过堆,内存管理器(Memory Mamager)将一块较大的内存空间委托给堆管理器(Heap Manager)来管理。堆管理器将大块的内存分割成不同大小的很多个小块来满足应用程序的需要。应用程序的内存需求同城是频繁而且零散的,如果把这些请求都直接传递给位于内核中的内存管理器,那必将影响系统的性能。有了堆管理器,内存管理器就只需要处理大规模的分配请求。这样做不经可以减轻内存管理器的负担,也可以大大缩短应用程序申请内存分配所需的时间,提高程序的运行速度。
用户态的代码应该调用虚拟内存分配API来从内存管理器分配内存。虚拟内存API包括VirtualAlloc,VirtualAllocEx,VirtualFree,VirtualFreeEx,VirtualLock,VirtualUnlock,VIrtualProtect,VIrtualQuery。
内核态的代码可以调用以上API所对应的内核函数,如NtAllocateVirtualMemory, NtProtectVirtualMemory。
为了满足内核空间中的驱动程序等内核态代码的内存分配需要,Windows的内核模块中实现了一系列函数来提供内存零售服务,为了与用户空间的堆管理器相区别,这些函数统称为池管理器(Pool Manager)。池管理器公开了一组驱动程序接口(DDI)以向外提供服务,包括ExAllocatePool,ExAllocatePoolWithTag等。
http://www.cnblogs.com/CBDoctor/archive/2013/02/20/2918456.html
(备注:虚拟内存API是通过内存管理器从虚拟内存中分配大块的内存)
与内核模块中的池管理器类似,在NTDLL.DLL中实现了一个通用的堆管理器,目的为用户态的应用程序提供内存服务,通常被称为Win32堆管理器。SDK中公开了一组API来访问Win32堆管理器的功能,如HeapAlloc,HeapFree等。
为了支持C的内存分配函数和C++的内存分配运算符(new和delete)(统称为CRT内存分配函数),编译器的C运行库会创建一个专门的堆供这些函数所使用,通常称为CRT堆。根据分配堆块的方式不同,CRT堆有三种工作模式:SBH(Small Block Heap),旧SBH(Old SBH)和系统模式(System Heap)。当创建CRT堆时,会选择其中的一种。对于前两种模式,CRT堆会使用虚拟内存分配API从内存管理器批发大的内存过来,然后分割成效的堆块满足应用程序的需要。对于系统模式,CRT堆只是把堆块分配请求转发给他所基于的Win32堆,因此处于系统模式的CRT堆只是对Win32堆得一种简单封装,在原来的基础上又增加了一些附加的功能。
应用程序开发商也可以实现自己的堆管理器,只要通过虚拟内存API从内存管理器批发内存块过来提供给自己的客户代码使用。
从实现角度来讲,内核态的池管理器和用户态的Win32堆管理器是共享一套基础代码,它们以运行时库的形式分别存在于NTOSKRNL.EXE和NTDLL.DLL模块中。
x ntdll!*Heap*
用户态和内核态中负责管理和操作堆得基本函数都是相同的。
2 堆的创建和销毁
2.1 进程的默认堆
Windows系统在创建一个新的进程时,在加载函数执行进程的用户态初始化阶段,会调用RtlCreateHeap函数为新的进程创建第一个堆,称为进程的默认堆,有时也称为进程堆(Process Heap)。
kb查看线程调用堆栈。
创建好的堆句柄会保存到进程环境块(PEB)的ProcessHeap字段中。可以使用WinDBG的dt命令来观察PEB结构:
0:000> .process
Implicit process is now 7ffd9000+0x240 SpareTracingBits : 0y000000000000000000000000000000 (0)
ProcessHeap是进程堆的句柄
HeapSegmentReserve是进程堆得保留大小,其默认值为1MB(即0x100000)
HeapSegmentCommit是进程堆得初始提交大小,其默认值为两个内存页大小,X86系统中普通内存页的大小为4KB,因此是0x2000,即8KB。可以通过链接选项/HEAP来改变进程堆得保留大小和初始提交大小:
/HEAP:reserve[, commit]
使用GetProcessHeap API可以获得当前进程的进程堆句柄:
HANDLE GetProcessHeap(void);
事实上,GetProcessHeap(void)函数只是简单的找到PEB结构,然后读出ProcessHeap字段的值。得到进程堆得句柄后,就可以使用HeapAlloc API从这个堆上申请空间,如:
CStructXXX* pStruct = HeapAlloc( GetProcessHeap(), 0, sizeof( CStructXXX ) );
2.2 创建私有堆
除了系统为每个进程创建的默认堆,应用程序也可以通过调用HeapCreate API创建其他堆,这样的堆只能被发起调用的进程自己访问,通常被称为私有堆(Private Heap)。
HeapCreate内部主要是调用RtlCreateHeap,因此私有堆与默认堆并没有本质的差异,只是创建的用途不同。RtlCreateHeap内部会调用ZwAllocateVirtualMemory系统服务从内存管理器申请内存空间,初始化用于维护堆得数据结构,最后将堆句柄记录到进程的PEB结构中。
2.3 堆列表
每个进程的PEB结构以列表的形式记录了当前进程的所有堆句柄,包括进程的默认堆。具体来说,PEB中有三个字段用于记录这些句柄:
NumberOfHeaps字段用来记录堆得总数
ProcessHeaps字段用来记录每个堆得句柄,它是一个数组,这个数组可以容纳的句柄数记录在MaximumNumberOfHeaps字段中。
如果NumberOfHeaps达到MaximumNumberOfHeaps,那么堆管理器会增大MaximumNumberOfHeaps值,并重新分配ProcessHeaps数组。
dd [ProcessHeaps首地址] 查看当前进程所有堆指针。
!heap -h 列出当前进程的所有堆
2.4 销毁堆
应用程序可以调用HeapDestroy API来销毁进程的私有堆。HeapDestroy内部主要是调用NTDLL中的RtlDestoryHeap函数。后者会从PEB的堆列表中将要销毁的堆句柄移除,然后调用NtFreeVirtualMemory向内存管理器归还内存。
应用程序不需要也不应该销毁进程的默认堆,因为进程内的很多系统函数会使用这个堆。不必担心这会导致内存泄露,因为当进程退出和销毁进程对象时,系统会两次调用内存管理器的MnCleanProcessAddressSpace函数来释放清理进程的内存空间。具体来说:
1.第一次是在退出进程中执行的,当NtTerminateProcess函数调用PspExitThread退出线程时,如果退出的是最后一个线程,那么PspExitThread会调用MmCleanProcessAddressSpace,后者会先删除进程用户空间中的文件映射和虚拟地址,释放虚拟地址描述符,然后删除进程空间的系统部分,最后删除进程的页表和页目录设施,这是在退出进程上下午中执行的最后几项任务之一。
2.第二次是当系统的工作线程删除进程对象时会再次调用MmCleanProcessAddressSpace函数。
3 分配和释放堆块
当应用程序调用堆管理器的分配函数向堆管理器申请内存时,堆管理器会从自己维护的内存区中分割出一个满足用户指定大小的内存块,然后把这个快中允许用户访问部分的起始地址返回给应用程序,堆管理器把这样的块叫做一个Chunk(堆块)应用程序用完一个堆块后,应该调用堆管理器的释放函数归还堆块。
3.1 HeapAlloc
HeapAlloc API只不过是RtlAllocHeap的一个别名,二者实际上是等价的。
3.2 CRT分配函数
编译器的运行时库在初始化阶段会创建CRT堆,创建前会选择三种模式之一,大多数选择的是系统分配模式,在这种情况下,CRT堆是建立在Win32堆之上的,所以CRT分配函数最终也是调用HeapAlloc函数来分配内存块的。
3.3 释放从堆中分配的内存
HeapFree,与HeapAlloc一样,都是被链接到NTDLL.DLL中的RtlAllocateHeap类似,HeapFree被链接到RtlFreeHeap函数。
3.4 GlobalAlloc LocalAlloc
16位Windows支持所谓的全局堆和局部堆,简单来说,局部堆是进程内的,全局堆是系统提供给所有进程来共享使用的。
NT系列的Windows不再支持全局堆,但为了保持与16位兼容,以上API任然保留,不过无论是GlobalAlloc还是LocalAlloc实际上都是从进程的默认堆上来分配内存。
3.5 解除提交
释放从堆上分配的内存并不意味着堆管理器会立刻把这个内存块所对应的空间立刻归还给系统的内存管理器。考虑到应用程序可能很快还会申请内存和减少与内存管理器的交互次数,堆管理器只在一下两个条件都满足才会立即调用ZwFreeVirtualMemory函数向内存管理器释放内存,通常称为解除提交(Decommit)。
第一个条件:本次释放的堆块大小超过了堆参数中的DeCommitFreeBlockThreshold所代表的阀值。
第二个条件:累计起来的总空闲时间(包括本次)超过了堆参数中的DeCommitTotalFreeThreshold所代表的阀值。
DeCommitFreeBlockThreshold和DeCommitTotalFreeThreshold是放在堆管理区的参数,创建堆时会用PEB结构中的HeapDeCommitFreeBlockThreshold和HeapDeCommitTotalFreeThreshold字段的值来初始化这两个参数。
+0x080 HeapDeCommitTotalFreeThreshold : 0x10000
+0x084 HeapDeCommitFreeBlockThreshold : 0x1000
也就是说,当要释放的堆块超过4KB并且堆上的总空闲空间达到64KB时,堆管理器才会立即向内存管理器执行解除提交操作真正释放内存,否则,堆管理器会将这个快加到空闲块列表中,并更新堆管理区的总空闲空间值(TotalFree)。
值得注意的是,堆管理器内部是以分配粒度为单位来表示上面说的那两个阀值和计算堆块大小的。调用GetSystemInfo API可以获得分配粒度,通常是8。WinDBG中使用!heap -v可以观察堆得分配粒度和解除提交阀值。
0:000> !heap 002d0000 -v
Index Address Name Debugging options enabled
1: 002d0000
Segment at 002d0000 to 003d0000 (00005000 bytes committed)
Flags: 40000062
ForceFlags: 40000060
Granularity: 8 bytes
Segment Reserve: 00100000
Segment Commit: 00002000
DeCommit Block Thres: 00000200
DeCommit Total Thres: 00002000
Total Free Size: 00000102
Max. Allocation Size: 7ffdefff
Lock Variable at: 002d0138
Next TagIndex: 0000
Maximum TagIndex: 0000
Tag Entries: 00000000
PsuedoTag Entries: 00000000
Virtual Alloc List: 002d00a0
Uncommitted ranges: 002d0090
FreeList[ 00 ] at 002d00c4: 002d47e8 . 002d3288 (2 blocks)
单块阀值参数等于0x200,因为它是以分配粒度8为单位的,所以将其乘以8便是0x1000,与PEB中的匹配。
4 堆的内部结构