为了写这篇博文,得借用张银奎所著的<软件调试>一书中第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 fill
windbg认为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)
Q3:现在知道了_heap!Segment中提供内存分配的能力,那么分配出去的内存又是怎样的结构?
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
不知道大家有没有发现 每次分配堆内存都有间隙:第一次请求0x10B,分配到的内存是0x142a80,第二次请求0x10B,分配到的内存是0x142aa8 ,中间0x28B是什么?看下windbg的输出:
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油尽灯枯~