为了写这篇博文,得借用张银奎所著的<软件调试>一书中第23章HiHeap.c作为demo程序。(我的环境是xp sp3+vc++6.0)
#include "stdafx.h" #define _WIN32_WINNT 0x0501 #include <windows.h> #include <crtdbg.h> #include <malloc.h> #include <stdio.h> #include <stdlib.h> //#include <winheap.h> //#include <dbgint.h> //HeapAlloc void EnumHeaps() { DWORD dwTotal,dwHeapComp; HANDLE *phHeaps; dwTotal=GetProcessHeaps(0,NULL); if(dwTotal==0) { TAG_ERROR: printf("GetProcessHeaps failed for %d.\n", GetLastError()); return; } phHeaps=(PHANDLE)new HANDLE[dwTotal]; dwTotal=GetProcessHeaps(dwTotal,phHeaps); if(dwTotal==0) goto TAG_ERROR; for(unsigned int i=0;i<dwTotal;i++) { #if WINVER>=0x0501 #if 0 if(HeapQueryInformation(phHeaps[i], HeapCompatibilityInformation, &dwHeapComp, sizeof(DWORD), NULL)) { printf("HeapCompatibilityInformation of Heap %8X is %d\n", phHeaps[i],dwHeapComp); } #endif #endif } delete phHeaps; } void TestAlloc(BOOL bLeak) { void * pStruct = HeapAlloc(GetProcessHeap(), 0, 0x10); #if 0 if(!bLeak) HeapFree(GetProcessHeap(),0, pStruct); #endif } void TestNew(BOOL bLeak) { char * lpsz=new char[2048]; if(!bLeak) delete lpsz; } void TestMalloc(BOOL bLeak) { void * p=malloc(5); if(!bLeak) free(p); } void TestAllocA(int n) { char * buf = (char*)_alloca( n ); // do something with buf //_freea( buf ); } void TestMallocDbg(int n) { char * buf=(char*)_malloc_dbg(10, 111, NULL, 0); strcpy(buf, "test"); } void CheckMem() { #ifdef _DEBUG _CrtMemState s; #endif _CrtMemCheckpoint(&s); _CrtMemDumpStatistics(&s); } void TestGlobal() { HGLOBAL hMemGlobal=GlobalAlloc(0, 111); GlobalFree(hMemGlobal); HLOCAL hMemLocal=LocalAlloc(0,111); LocalFree(hMemLocal); } void TestVirtualAlloc(DWORD dwGranularity) { ULONG ulSize=1<<16<<dwGranularity; PVOID pMem=HeapAlloc(GetProcessHeap(),0,ulSize); if(IsDebuggerPresent()) DebugBreak(); HeapFree(GetProcessHeap(),0, pMem); } // do allocations so that grow heap with more segments void TrigerMulSegment() { char c=0; PVOID pMem; ULONG ulSize=0xf000*8; // about 480KB // should be less than 0xfe00*8 to avoid virtal alloc directly while(c!='b') { pMem=HeapAlloc(GetProcessHeap(),0,ulSize); printf("Allocated %d at 0x%x. Enter 'b' to abort\n", ulSize, pMem); // there will be memory leak here, anyway... c=getchar(); } } void TestDecommit(ULONG ulSize) { printf("Any key to alloc %d bytes on heap.\n", ulSize); getchar(); PVOID pMem=HeapAlloc(GetProcessHeap(),0,ulSize); printf("Allocate memroy at 0x%x, any key to free it.\n",pMem); getchar(); HeapFree(GetProcessHeap(),0,pMem); printf("Memroy is freed, any key to continue.\n"); getchar(); } int main(int argc, char* argv[]) { SYSTEM_INFO sSysInfo; // useful system information char opt; GetSystemInfo(&sSysInfo); // fill the system information structure printf("Page Size=%d, Granularity=%d\n", sSysInfo.dwPageSize, sSysInfo.dwAllocationGranularity); while(1) { opt = 0; printf("opt\n"); scanf("%c",&opt); switch(opt) { case 'v': TestVirtualAlloc(8); break; case 'g': TestGlobal(); break; case 'd': TestDecommit(argc>2?atoi(argv[2]):0x1008); break; case 's': TrigerMulSegment(); break; case 'a': TestAlloc(FALSE); break; case 'n': TestNew(FALSE); break; case 'm': TestMalloc(TRUE); TestMallocDbg(FALSE); break; case 'c': CheckMem(); break; default: printf("bad command %s\n",argv[1]); } } _CrtDumpMemoryLeaks(); return 0; }Q1:进程在哪以什么形式记录进程堆?
A1:进程PEB结构中记录了进程使用的堆。
0:000> dt _peb @$peb ntdll!_PEB+0x090 ProcessHeaps : 0x7c99ffe0 -> 0x00150000 Void 0:000> dd 7c99ffe0 l8 0:000> dd 7c99ffe0 l8 7c99ffe0 00150000 00250000 00260000 00000000 7c99fff0 00000000 00000000 00000000 00000000
进程PEB!ProcessHeaps记录了进程堆数组的地址,每个数组项记录了进程创建的堆。如这里的输出,进程创建的堆句柄(其实就是堆地址)为:0x150000,0x250000,0x260000。用!heap扩展命令可以验证这个结果。
0:000> !heap NtGlobalFlag enables following debugging aids for new heaps: tail checking free checking validate parameters Index Address Name Debugging options enabled 1: 00150000 tail checking free checking validate parameters 2: 00250000 tail checking free checking validate parameters 3: 00260000 tail checking free checking validate parameters可以看到程序启动时,共创建了三个堆和通过PEB分析的结果相同。
Q2:堆管理器如何管理堆中的内存?
A2:_HEAP是堆结构,里面有若干重要字段:
0:000> dt _heap ntdll!_HEAP +0x014 VirtualMemoryThreshold: Uint4B +0x050 VirtualAllocdBlocks : _LIST_ENTRY +0x058 Segments : [64] Ptr32 _HEAP_SEGMENT +0x178 FreeLists : [128] _LIST_ENTRY
当申请的堆内存大小大于VirtualMemoryThreshold的值,这些内存不从以_HEAP_SEGMENT结构的Segment中分配而是单独申请,并最终链入VirtualAllocdBlocks指向的队列进行管理。这就是大块内存管理;对于其他申请少量内存,则从Segment段分配。程序启动时默认使用Segment00段,当段中内存使用完,堆管理器为之新分配一个Segment段,并加入到_heap段数组中。从结构的定义可以看出,一个堆最多有64个段;与申请内存对应,HeapFree释放内存后,该内存被置为空闲并加入_heap的空闲块链表FreeList[N]中。堆初始时只有FreeList[0]链表非空,指向一大块用于分配的空闲内存,而其他FreeList[N]全是空队列,随着程序中内存的周转,FreeList[N]中的元素开始丰满起来。
来看看windbg对进程默认堆的分析结果:
0:000> !heap -hd 00140000 Index Address Name Debugging options enabled 1: 00140000 Segment at 00140000 to 00240000 (00003000 bytes committed) FreeList Usage: 00000000 00000000 00000000 00000000 FreeList[ 00 ] at 00140178: 00142990 . 00142990 (1 block ) Heap entries for Segment00 in Heap 00140000 00140640: 00640 . 00040 [01] - busy (40) 00140680: 00040 . 01818 [07] - busy (1800), tail fill - unable to read heap entry extra at 00141e90 00141e98: 01818 . 00040 [07] - busy (22), tail fill - unable to read heap entry extra at 00141ed0 00141ed8: 00040 . 00048 [07] - busy (2b), tail fill - unable to read heap entry extra at 00141f18 00141f20: 00048 . 002f0 [07] - busy (2d8), tail fill - unable to read heap entry extra at 00142208 00142210: 002f0 . 00330 [07] - busy (314), tail fill - unable to read heap entry extra at 00142538 00142540: 00330 . 00330 [07] - busy (314), tail fill - unable to read heap entry extra at 00142868 00142870: 00330 . 00040 [07] - busy (24), tail fill - unable to read heap entry extra at 001428a8 001428b0: 00040 . 00028 [07] - busy (10), tail fill - unable to read heap entry extra at 001428d0 001428d8: 00028 . 00058 [07] - busy (40), tail fill - unable to read heap entry extra at 00142928 00142930: 00058 . 00058 [07] - busy (40), tail fill - unable to read heap entry extra at 00142980 00142988: 00058 . 00678 [14] free fill 00143000: 000fd000 - uncommitted bytes.从windbg输出情况可以看到程序启动时FreeList[0]只有一个空闲块,指向0x0142990
00142988: 00058 . 00678 [14] free fillwindbg认为0x142988是空闲内存,和上面分析的0x0142990有出入,这个下面将解释原因。
上面是!heap扩展命令的结果,手动分析0x00140000处的内存值,对比一下是否有出入:
dt _heap 00140000 ntdll!_HEAP +0x014 VirtualMemoryThreshold : 0xfe00 ;分配粒度,换算实际内存时需要乘以8 +0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x140050 - 0x140050 ] ;由于链表头指向自身,所以暂时是空队列 +0x058 Segments : [64] 0x00140640 _HEAP_SEGMENT +0x178 FreeLists : [128] _LIST_ENTRY [ 0x142990 - 0x142990 ] 0:000> dd 00140058 l8 ;进程堆_heap中的段数组 00140058 00140640 00000000 00000000 00000000 ;程序运行前 进程堆只有一个段--Segment00段 0:000> dt _LIST_ENTRY 00140000+0x178 ntdll!_LIST_ENTRY [ 0x142990 - 0x142990 ] +0x000 Flink : 0x00142990 _LIST_ENTRY [ 0x140178 - 0x140178 ] +0x004 Blink : 0x00142990 _LIST_ENTRY [ 0x140178 - 0x140178 ] ;LIST_ENTRY[0]非空,指向一片空闲内存 0:000> dt _LIST_ENTRY 00140000+0x178 +8 ntdll!_LIST_ENTRY [ 0x140180 - 0x140180 ] +0x000 Flink : 0x00140180 _LIST_ENTRY [ 0x140180 - 0x140180 ] +0x004 Blink : 0x00140180 _LIST_ENTRY [ 0x140180 - 0x140180 ] ;LIST_ENTRY[1]空,指向自身 0:000> dt _heap_segment 00140640 ;这段内存中是组_HEAP_ENTRY结构 通过_HEAP_ENTRY的Size和PrevSize域可以遍历整个_HEAP_SEGMENT的所有堆块 ntdll!_HEAP_SEGMENT +0x020 FirstEntry : 0x00140680 _HEAP_ENTRY 0:000> dt _heap_entry 0x00140680 ntdll!_HEAP_ENTRY +0x000 Size : 0x303 ;当前堆块的大小粒度,*8=0x1818 +0x002 PreviousSize : 8 ;前一个堆块的大小粒度 =0x40 ;这个输出正好和windbg !heap扩展命令前两个项的大小结果相同 <pre name="code" class="cpp">Heap entries for Segment00 in Heap 00140000 00140640: 00640 . 00040 [01] - busy (40) 00140680: 00040 . 01818 [07] - busy (1800)
A3:这个得分情况讨论,这里讨论Debug模式下的堆分配结构,Release的可以通过windbg attach的方式调试分析。选择选项a,观察其结果。
在代码
void * pStruct = HeapAlloc(GetProcessHeap(), 0, 0x10);
处下断点,运行到这行后观察pStruct分配到的堆内存:
:000> dd pStruct 0012fef4 00142a80 ;分配的内存是0x142a80 0:000> dd 00142a80 00142a80 baadf00d baadf00d baadf00d baadf00d 00142a90 abababab abababab 00000000 00000000然后f5运行继续输入选相a,看下这次分配到什么
0:000> dd pStruct 0012fef4 00142aa8 0:000> dd 00142aa8 l8 00142aa8 baadf00d baadf00d baadf00d baadf00d 00142ab8 abababab abababab 00000000 00000000
0:000> dd 00142a90 00142a90 abababab abababab 00000000 00000000 00142aa0 00050005 001807e9其实,这个分成两部分:从0x0142a90 到0x0142a9F这0x10字节是第一次请求0x10B字节堆内存的后置填充数据;从0x0142aa0到 0x0142aa7这0x8B字节是第二次请求0x10B字节堆内存的前置填充数据,这是一个_HEAP_ENTRY结构,用来表明这次分配内存的大小和上一次分配内存的大小。因此一段请求的堆内存其实由3部分组成:
前置_HEAP_ENTRY结构(8字节)--用户可用区代码(调试状态时被baadfood填充,release模式时,因为不管release还是debug模式,堆管理器占用用户区内取前8字节作为_LIST_ENTRY结构,只是release模式中没有被填充数覆盖)--后置填充结构。
最后,来看下经过这几次堆内存分配,FreeList[0]的内容:
0:000> dt _heap 00140000 ntdll!_HEAP +0x000 Entry : _HEAP_ENTRY +0x178 FreeLists : [128] _LIST_ENTRY [ 0x142ad0 - 0x142ad0 ]可以看到此时FreeList[0]指向0x142ad0。前面说过,不管release还是debug模式,堆管理器占用用户区内取前8字节作为_LIST_ENTRY结构,由此可以猜测下次分配到的内存值是0x142ad0。同时还能断定0x142ac8开始的8个字节是个空闲的_HEAP_ENTRY结构。来用windbg验证一下:
0:000> dt _HEAP_ENTRY 0x142ac8 ntdll!_HEAP_ENTRY +0x000 Size : 0xa7 +0x002 PreviousSize : 5 ;前一次分配出去40B字节:8字节前置数据+16字节用户数据+16字节后置数据F5运行,输入选项a查看内存分配情况,的确是0x142ad0,我没骗你们~
写在最后,如果想测试一下大块内存分配结果,可以输入选项v
如果想测试耗完当前_HEAP_SEGMENY从而分配新_HEAP_SEGMENY,可以输入s选项并不断的输入b,几个回合就能让segment00油尽灯枯~