windbg学习实例2:堆的手工分析(AWDDBG学习笔记)

大多数(并不是所有)高层的内存管理器都使用了Windows堆管理器,而堆管理器又会使用虚拟内存管理器:
下图给出了在Windows中支持的内存管理器以及它们之间的关系:

当进程启动时,堆管理器将自动创建一个新堆,叫做默认的进程堆,但是new/delete运算符以及malloc/free等API仍使用CRT堆来满足它们的内存需求,有些进程还会创建一些额外的堆(通过HeapCreate)以将进程中不同的组件独立开来

可以把Windows堆管理器做进一步的划分,如下图:

前端分配器

前端分配器(Front End Allocator是对后端分配器(Back End Allocator)的一个抽象优化层,在Windows中有两种前端分配器:

旁视列表(Look Aside List, LAL)前端分配器

低碎片(Low Fragmentation, LF)前端分配器

除了Vista外,所有版本Windows在默认情况下都使用LAL前端分配器,下面介绍LAL前端分配器:

LAL是一张表,其中有128项(可以想像成128行),每一项对应于一个单向链表(相当于列),每个单向链表都包含了一组固定大小的空闲堆块,从16字节

的大小开始依次递增,(堆块可以想像成i行j列中元素的大小,可能为空,如有值,最小值为16),每个堆块中包含了8字节的堆块元数据用于管理这个堆块,

所以,如果用户请求24字节分配,那么LAL前先查找大小为32字节(用户请求24字节+8字节元数据)的空闲堆块,由于LAL的索引0项大小为8字节的空闲堆块

(假想成第0行的元素要么为空,要么为8),所以它不会被使用

行的元素值是向下递增的(8字节递增),最后一个索引(127)包含了大小为1024字节的空闲堆块,但程序释放一个内存块时,堆管理器将把这块内存标记为空闲的,

然后将它放在LAL相应的位置(按大小),下次请求这个大小的内存块(这个大小=用户请求字节+8字节,精确匹配,比如,24的不能分配32的)时,LAL将检查是否存在大小相同的空闲堆块,如果存在,就把这个堆块返回给用户,到目前为止,

LAL是满足内存分配需求的最快内存分配方式

如果LAL无法满足要求,那么这个请求将被转发到后端分配器:

后端分配器

和LAL同样的,后端分配器也包含了一张空闲列表(Free Lists)这张表和LAL稍有不同,就是空闲列表[2]的堆块大小为16(第三行,而LAL是第二行)之后同样以8字节 行递增

同样道理,空闲列表[1]没有被使用(8字节元数据,没有用户使用空间了),同样它也是128项,那么明显最后行为1016字节的堆块,

在空闲列表[0]中的堆块大小将大于1016字节而小于虚拟内存分配的限值0x7FFF0(这一行的元素堆大小为自由大小,不需要是8的倍数,但升序排列以获得最大效率)

为了更有效的查找,堆管理器而将维持一个空闲列表位图,位图包含128位(对应相应的128行),如行元素全为空,则对应的位为0,否则为1

好吧,典型的哈希映射,

如果堆管理器还是无法找到某个空闲堆块的大小等于所请求的大小,然后它将使用一种块分割,块分割指堆管理器首先找到一个比请求大小更大的空闲堆块,然后将其对半分割以满足分配请求(是对半分割),

所有大于0x7fff0的内存分配请求将被转发到虚拟分配链表(Virtual Allocation List)中,当请求一个很大的内存分配时,堆管理器将向虚拟内存管理器发出请求,并且将相应的分配信息保留在虚拟分配链表中,

堆管理器从何处获得内存?

首先,堆管理器将通过Windows虚拟内存管理器来分配一大块内存,然后,这个内存将被分为不同大小的块以满足程序的内存分配请求,当这块内块被耗尽时,堆管理器又将从虚拟内存中分配一大块内存,然后这个过程将重复进行,堆管理器从虚拟内存管理器请求分配的内存块也称为堆段,当堆段最被创建时,堆段中的大部分虚拟内存都是被保留的(Reserve),只有一小部分被提交,当堆管理器耗尽了已提交的空间时,堆段将进一步提交更多的内存,而堆管理器将对新提交的空间进行分割,

当一个堆段耗尽所有空间后,堆管理器将创建另一个新的堆段,新堆段大小将是之前堆段的二倍,如果内存不足,然后堆管理器将把这个大小减半,再失败,再减半,直到

堆段大小的阈值,这时将返回一个错误给用户,在堆中最多可以同时存在64个堆段,同样,堆管理器弄了个链表来管理它们,

实例分析

先上代码:

#include "stdafx.h"
#include <windows.h>

void __cdecl wmain (int argc, WCHAR* args[])
{
    BYTE* pAlloc1=NULL;
    BYTE* pAlloc2=NULL;
    HANDLE hProcessHeap=GetProcessHeap();

    pAlloc1=(BYTE*) HeapAlloc(hProcessHeap, 0, 16);
    pAlloc2=(BYTE*) HeapAlloc(hProcessHeap, 0, 1500);

    // 
    // Use allocated memory
    // 
    HeapFree(hProcessHeap, 0, pAlloc1);
    HeapFree(hProcessHeap, 0, pAlloc2);
}

Release版本,Windbg加载符号文件运行:

$peb当前进程的进程环境块(PEB)的地址

我们把当前的进程环境块解析出来:

0:000> dt _PEB @$peb
test1!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 SpareBool        : 0 ''
   +0x004 Mutant           : 0xffffffff 
   +0x008 ImageBaseAddress : 0x00400000 
   +0x00c Ldr              : 0x00251ea0 _PEB_LDR_DATA
   +0x010 ProcessParameters : 0x00020000 _RTL_USER_PROCESS_PARAMETERS
   +0x014 SubSystemData    : (null) 
   +0x018 ProcessHeap      : 0x00150000 
   +0x01c FastPebLock      : 0x7c9a0620 _RTL_CRITICAL_SECTION
   +0x020 SparePtr1        : 0x7c921000 
   +0x024 SparePtr2        : 0x7c9210e0 
   +0x028 EnvironmentUpdateCount : 1
   +0x02c KernelCallbackTable : (null) 
   +0x030 SystemReserved   : [1] 0
   +0x034 ExecuteOptions   : 0y00
   +0x034 SpareBits        : 0y000000000000000000000000000000 (0)
   +0x038 FreeList         : (null) 
   +0x03c TlsExpansionCounter : 0
   +0x040 TlsBitmap        : 0x7c9a05e0 
   +0x044 TlsBitmapBits    : [2] 7
   +0x04c ReadOnlySharedMemoryBase : 0x7f6f0000 
   +0x050 ReadOnlySharedMemoryHeap : 0x7f6f0000 
   +0x054 ReadOnlyStaticServerData : 0x7f6f0688  -> (null) 
   +0x058 AnsiCodePageData : 0x7ffa0000 
   +0x05c OemCodePageData  : 0x7ffa0000 
   +0x060 UnicodeCaseTableData : 0x7ffd1000 
   +0x064 NumberOfProcessors : 4
   +0x068 NtGlobalFlag     : 0x70
   +0x070 CriticalSectionTimeout : _LARGE_INTEGER 0xffffe86d`079b8000
   +0x078 HeapSegmentReserve : 0x100000
   +0x07c HeapSegmentCommit : 0x2000
   +0x080 HeapDeCommitTotalFreeThreshold : 0x10000
   +0x084 HeapDeCommitFreeBlockThreshold : 0x1000
   +0x088 NumberOfHeaps    : 4
   +0x08c MaximumNumberOfHeaps : 0x10
   +0x090 ProcessHeaps     : 0x7c99ffe0  -> 0x00150000 
   +0x094 GdiSharedHandleTable : (null) 
   +0x098 ProcessStarterHelper : (null) 
   +0x09c GdiDCAttributeList : 0
   +0x0a0 LoaderLock       : 0x7c99e174 _RTL_CRITICAL_SECTION
   +0x0a4 OSMajorVersion   : 5
   +0x0a8 OSMinorVersion   : 1
   +0x0ac OSBuildNumber    : 0xa28
   +0x0ae OSCSDVersion     : 0x300
   +0x0b0 OSPlatformId     : 2
   +0x0b4 ImageSubsystem   : 3
   +0x0b8 ImageSubsystemMajorVersion : 5
   +0x0bc ImageSubsystemMinorVersion : 0
   +0x0c0 ImageProcessAffinityMask : 0
   +0x0c4 GdiHandleBuffer  : [34] 0
   +0x14c PostProcessInitRoutine : (null) 
   +0x150 TlsExpansionBitmap : 0x7c9a05d8 
   +0x154 TlsExpansionBitmapBits : [32] 0
   +0x1d4 SessionId        : 0
   +0x1d8 AppCompatFlags   : _ULARGE_INTEGER 0x0
   +0x1e0 AppCompatFlagsUser : _ULARGE_INTEGER 0x0
   +0x1e8 pShimData        : (null) 
   +0x1ec AppCompatInfo    : (null) 
   +0x1f0 CSDVersion       : _UNICODE_STRING "Service Pack 3"
   +0x1f8 ActivationContextData : 0x00140000 _ACTIVATION_CONTEXT_DATA
   +0x1fc ProcessAssemblyStorageMap : 0x00152988 _ASSEMBLY_STORAGE_MAP
   +0x200 SystemDefaultActivationContextData : 0x00130000 _ACTIVATION_CONTEXT_DATA
   +0x204 SystemAssemblyStorageMap : (null) 
   +0x208 MinimumStackCommit : 0
   +0x20c FlsCallback      : (null) 
   +0x210 FlsListHead      : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x218 FlsBitmap        : (null) 
   +0x21c FlsBitmapBits    : [4] 0
   +0x22c FlsHighIndex     : 0


为什么要使用@$peb呢:windbg帮助文档提到了:

When you use a register in an expression, you should add an at sign ( @ ) before the register. This at sign tells the debugger that the following text is the name of a register.

If you are using MASM expression syntax, you can omit the at sign for certain very common registers. On x86-based systems, you can omit the at sign for theeax,ebx, ecx, edx,esi,edi, ebp, eip, andefl registers. However, if you specify a less common register without an at sign, the debugger first tries to interpret the text as a hexadecimal number. If the text contains non-hexadecimal characters, the debugger next interprets the text as a symbol. Finally, if the debugger does not find a symbol match, the debugger interprets the number as a register.

If you are using C++ expression syntax, the at sign is always required.

所以使用时都加上@吧,

最前面一排表示的是相对于这个结构首地址的偏移地址,首先我们关心的是位于偏移0x90处的进程堆链表,PEB中这个堆栈表成员是一个指针数组,每个指针都指向一个类型为_HEAP的数据结构:

   +0x090 ProcessHeaps     : 0x7c99ffe0  -> 0x00150000 

转储出来:

0:000> dd 0x7c99ffe0  
7c99ffe0  00150000 00250000 00260000 00390000
7c99fff0  00000000 00000000 00000000 00000000
7c9a0000  00000000 00000000 00000000 00000000
7c9a0010  00000000 00000000 00000000 00000000
7c9a0020  05d205d0 00020498 00000001 7c9b6000
7c9a0030  7ffd1de6 00000001 00000005 00000001
7c9a0040  fffff739 00000000 003a0043 0057005c
7c9a0050  004e0049 004f0044 00530057 0073005c

从信息来看,进程中共有四个堆,这个可以从最上面的

   +0x088 NumberOfHeaps    : 4

看出,并且默认进程堆总是位为链表的第一项:其实默认进程堆也可以从这看出:

   +0x018 ProcessHeap      : 0x00150000 

进程中存在多个堆是很正常的,比如C运行时,这个组件在初始化时将创建自己的堆

由于程序中使用默认进程堆,因此我们重点对这个堆进行分析,把这个地址按_HEAP结构解析出来:

0:000> dt _HEAP 0x00150000 
ntdll!_HEAP
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : 0xeeffeeff
   +0x00c Flags            : 0x50000062
   +0x010 ForceFlags       : 0x40000060
   +0x014 VirtualMemoryThreshold : 0xfe00
   +0x018 SegmentReserve   : 0x100000
   +0x01c SegmentCommit    : 0x2000
   +0x020 DeCommitFreeBlockThreshold : 0x200
   +0x024 DeCommitTotalFreeThreshold : 0x2000
   +0x028 TotalFreeSize    : 0x292
   +0x02c MaximumAllocationSize : 0x7ffdefff
   +0x030 ProcessHeapsListIndex : 1
   +0x032 HeaderValidateLength : 0x608
   +0x034 HeaderValidateCopy : (null) 
   +0x038 NextAvailableTagIndex : 0
   +0x03a MaximumTagIndex  : 0
   +0x03c TagEntries       : (null) 
   +0x040 UCRSegments      : (null) 
   +0x044 UnusedUnCommittedRanges : 0x00150598 _HEAP_UNCOMMMTTED_RANGE
   +0x048 AlignRound       : 0x17
   +0x04c AlignMask        : 0xfffffff8
   +0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x150050 - 0x150050 ]
   +0x058 Segments         : [64] 0x00150640 _HEAP_SEGMENT
   +0x158 u                : __unnamed
   +0x168 u2               : __unnamed
   +0x16a AllocatorBackTraceIndex : 0
   +0x16c NonDedicatedListLength : 1
   +0x170 LargeBlocksIndex : (null) 
   +0x174 PseudoTagEntries : (null) 
   +0x178 FreeLists        : [128] _LIST_ENTRY [ 0x152b78 - 0x152b78 ]

+0x578 LockVariable : 0x00150608 _HEAP_LOCK +0x57c CommitRoutine : (null) +0x580 FrontEndHeap : 0x00150688 +0x584 FrontHeapLockCount : 0 +0x586 FrontEndHeapType : 0x1 '' +0x587 LastSegmentIndex : 0 ''

其中空闲列表在这:

  +0x178 FreeLists        : [128] _LIST_ENTRY [ 0x152b78 - 0x152b78 ]

其中包含一组双向链表,每个链表都包含了固定大小的一组堆块,我们将进一步查看这些空闲链表:





下一个HeapAlloc调用将分配1500字节的内存,这个大小超出了空闲列表[0]的大小,所以,堆管理器将不得不从堆段中提交更多的内存,为了更好的理解堆段状态,我们可以手动来分析堆段,_HEAP结构中就包含了一组指针指向当前堆中所有活跃进的堆段,这个数组偏移位置是0x58

0:000> dd 0x00150000+0X58
00150058  00150640 00000000 00000000 00000000
00150068  00000000 00000000 00000000 00000000
00150078  00000000 00000000 00000000 00000000
00150088  00000000 00000000 00000000 00000000
00150098  00000000 00000000 00000000 00000000
001500a8  00000000 00000000 00000000 00000000
001500b8  00000000 00000000 00000000 00000000

打印出来的为指向_HEAP_SEGMENT的地址,继续解析这个结构体:

0:000> dt _HEAP_SEGMENT 00150640 
ntdll!_HEAP_SEGMENT
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : 0xffeeffee
   +0x00c Flags            : 0
   +0x010 Heap             : 0x00150000 _HEAP
   +0x014 LargestUnCommittedRange : 0xfc000
   +0x018 BaseAddress      : 0x00150000 
   +0x01c NumberOfPages    : 0x100
   +0x020 FirstEntry       : 0x00150680 _HEAP_ENTRY
   +0x024 LastValidEntry   : 0x00250000 _HEAP_ENTRY
   +0x028 NumberOfUnCommittedPages : 0xfc
   +0x02c NumberOfUnCommittedRanges : 1
   +0x030 UnCommittedRanges : 0x00150588 _HEAP_UNCOMMMTTED_RANGE
   +0x034 AllocatorBackTraceIndex : 0
   +0x036 Reserved         : 0
   +0x038 LastEntryInSegment : 0x00152dc8 _HEAP_ENTRY


这里最有用的是FirstEntry,它表示在堆段中的第一个堆块,如果将这个堆块大小得到,那么和第一个堆块地址相加就可以得到下一个堆块,重复这个过程,就可以遍历整个堆段,并可以进一步分析每个堆块的正确性:

0:000> dt _HEAP_ENTRY 0x00150680 
ntdll!_HEAP_ENTRY
   +0x000 Size             : 0x303
   +0x002 PreviousSize     : 8
   +0x000 SubSegmentCode   : 0x00080303 
   +0x004 SmallTagIndex    : 0x57 'W'
   +0x005 Flags            : 0x7 ''
   +0x006 UnusedBytes      : 0x18 ''
   +0x007 SegmentIndex     : 0 ''
0:000> dt _HEAP_ENTRY 0x00150680+(0x303*8)
ntdll!_HEAP_ENTRY
   +0x000 Size             : 8
   +0x002 PreviousSize     : 0x303
   +0x000 SubSegmentCode   : 0x03030008 
   +0x004 SmallTagIndex    : 0x54 'T'
   +0x005 Flags            : 0x7 ''
   +0x006 UnusedBytes      : 0x1e ''
   +0x007 SegmentIndex     : 0 ''



 

原文地址:https://www.cnblogs.com/hgy413/p/3693511.html