天天看点

windows 进程堆结构梳理

    为了写这篇博文,得借用张银奎所著的<软件调试>一书中第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,我没骗你们~

windows 进程堆结构梳理

写在最后,如果想测试一下大块内存分配结果,可以输入选项v

如果想测试耗完当前_HEAP_SEGMENY从而分配新_HEAP_SEGMENY,可以输入s选项并不断的输入b,几个回合就能让segment00油尽灯枯~