堆调试
《软件调试读书笔记》第23章 - 堆和堆检查
0x1 大纲
- win32堆的创建、使用、内部结构、低碎片堆、调试支持
- CRT堆的概况、结构、调试支持
0x2 堆概念
堆分配部分API
VirtualAlloc | VirtualAllocEx |
---|---|
VirtualFree | VirtualFreeEx |
VirtualLock | VirtualLockEx |
VirtualProtect | VirtualQuery |
NtAllocateVirtualMemory | NtProtectVirtualMemory |
堆创建
系统创建新进程时,会调用RtlCreateHeap
创建此进程的第一个堆,也是进程的默认堆。下面的命令可以看到默认堆的创建
0:000> bu ntdll!RtlCreateHeap
0:000> .restart
0:000> kb
ChildEBP RetAddr Args to Child
0007fb04 7c9418bf 00000002 00000000 00100000 ntdll!RtlCreateHeap
0007fc94 7c94108f 0007fd30 7c920000 0007fce0 ntdll!LdrpInitializeProcess+0x4b7
0007fd1c 7c92e437 0007fd30 7c920000 00000000 ntdll!_LdrpInitialize+0x183
00000000 00000000 00000000 00000000 00000000 ntdll!KiUserApcDispatcher+0x7
我们可以通过PEB的结构看到默认堆的值
0:000> r @$peb
$peb=7ffd8000
0:000> dt _PEB 7ffd8000
……
……
+0x018 ProcessHeap : (null)
此时ntdll!RtlCreateHeap还没有完成,所以看不到,等初始化的函数完成了,就可以看到了
0:001> dt _PEB 7ffd8000
……
……
+0x018 ProcessHeap : 0x000a0000 //默认堆句柄
+0x078 HeapSegmentReserve : 0x100000 //堆的默认保留大小
+0x07c HeapSegmentCommit : 0x2000 //堆的默认提交大小
GetProcessHeap
可以拿到默认堆句柄,接下来就可以使用HeapAlloc
在这个堆上申请内存了。
除了默认堆之外还有私有堆,即用户使用HeapCreate
创建的堆
观察堆
0:001> dt _PEB 7ffd8000
+0x018 ProcessHeap : 0x000a0000
+0x088 NumberOfHeaps : 9 //当前堆的总数
+0x08c MaximumNumberOfHeaps : 0x10 //ProcessHeaps数组大小
+0x090 ProcessHeaps : 0x7c99cfc0 -> 0x001a0000 //指向堆句柄数组的指针
0:001> dd 0x7c99cfc0 l9
7c99cfc0 001a0000 003b0000 003c0000 00720000
7c99cfd0 00830000 00950000 00030000 005f0000
7c99cfe0 00fa0000
还有一种更方便的方法查看堆
0:001> !heap
Index Address Name Debugging options enabled
1: 001a0000
2: 003b0000
3: 003c0000
4: 00720000
5: 00830000
6: 00950000
7: 00030000
8: 005f0000
9: 00fa0000
观察堆的参数信息
0:001> !heap 1a0000 -v // 堆句柄
Index Address Name Debugging options enabled //开启的调试选项为空
1: 001a0000
Segment at 001a0000 to 002a0000 (00003000 bytes committed)//堆的内存段范围
Flags: 00000002 //堆标志,2是自动增长
ForceFlags: 00000000
Granularity: 8 bytes //堆分配粒度
Segment Reserve: 00100000
Segment Commit: 00002000
DeCommit Block Thres: 00000200
DeCommit Total Thres: 00002000
Total Free Size: 0000022f
Max. Allocation Size: 7ffdefff
Lock Variable at: 001a0608
Next TagIndex: 0000
Maximum TagIndex: 0000
Tag Entries: 00000000
PsuedoTag Entries: 00000000
Virtual Alloc List: 001a0050
UCR FreeList: 001a0598
FreeList Usage: 00000000 00000000 00000000 00000000
FreeList[ 00 ] at 001a0178: 001a1e90 . 001a1e90 (1 block )
堆内部结构
分段:每个堆至少有一个segment
,即0号segment,最多有64个segment。堆管理器在创建堆的时候会建立一个segment,在一个segment用完之后,如果这个堆是可增长的
,即堆标志为2,堆管理器会再分配一个段。
在0号段的头部,是一个heap
结构,定义了很多字段,记录了这个堆的一些属性。位于heap
结构之后的segment数组,记录了这个堆里的所有段。对于0号段以外的段来说,因为没有heap
结构,所以位于段的起始处。
再往下分,就是分成一系列不同大小的堆块。每个堆块的起始处是一个8字节的HEAP_ENTRY
结构,后面接着真正的数据区。HEAP_ENTRY
的前2个字节是表示堆的大小,单位是以堆的分配粒度为单位。
当一个app要分配超过512内存时,会直接调用ZwAllocVirtualMemory
来满足这次分配,分到的地址记录在HEAP结构的VirtualAllocBlocks
指向的链表中。也就是说,堆管理器向内存管理器申请内存时有两种渠道,一种是普通情况,堆块的大小比较小的时候,堆管理器直接批发一大批内存,也就是segment过来,再分给应用程序;再一种就是特殊情况特殊处理,遇到大客户的时候,超过512KB,那么堆管理器自己hold不住,就直接向内存管理器汇报申请了。
前端堆:segment结构后面接着的就是前段堆。作用是,小堆块释放后,当下次再需要分配时,就从这个前端堆列表里优先找到一个符合条件的堆块进行分配。
HEAP结构
CreteHeap返回的堆句柄,就是指向HEAP结构的指针。
0:001> dt 1a0000 _HEAP
+0x000 Entry : _HEAP_ENTRY //_HEAP数据结构的的堆块结构
+0x008 Signature : 0xeeffeeff
+0x00c Flags : 2 //HEAP_GROWABLE 可增长堆块
+0x010 ForceFlags : 0
+0x014 VirtualMemoryThreshold : 0xfe00 //可以在段中分配的堆块最大值
+0x018 SegmentReserve : 0x100000
+0x01c SegmentCommit : 0x2000
+0x020 DeCommitFreeBlockThreshold : 0x200
+0x024 DeCommitTotalFreeThreshold : 0x2000
+0x028 TotalFreeSize : 0x22f
+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 : 0x001a0598 _HEAP_UNCOMMMTTED_RANGE
+0x048 AlignRound : 0xf
+0x04c AlignMask : 0xfffffff8
+0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x1a0050 - 0x1a0050 ]
+0x058 Segments : [64] 0x001a0640 _HEAP_SEGMENT // 记录堆中包含的所有段,指针数组,每个指针指向一个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 [ 0x1a1e90 - 0x1a1e90 ] //空闲块
+0x578 LockVariable : 0x001a0608 _HEAP_LOCK
+0x57c CommitRoutine : (null)
+0x580 FrontEndHeap : 0x001a0688
+0x584 FrontHeapLockCount : 0
+0x586 FrontEndHeapType : 0x1 ''
+0x587 LastSegmentIndex : 0 '' //最后一个段的索引号
_HEAP_SEGMENT
数据结构
0:001> dt _HEAP_SEGMENT 0x001a0640 //_HEAP结构+0x058处
+0x000 Entry : _HEAP_ENTRY // _HEAP_SEGMENT结构的_HEAP_ENTRY
+0x008 Signature : 0xffeeffee
+0x00c Flags : 0
+0x010 Heap : 0x001a0000 _HEAP // segment所在的HEAP
+0x014 LargestUnCommittedRange : 0xfd000
+0x018 BaseAddress : 0x001a0000
+0x01c NumberOfPages : 0x100 // 段的内存页数
+0x020 FirstEntry : 0x001a0680 _HEAP_ENTRY
+0x024 LastValidEntry : 0x002a0000 _HEAP_ENTRY
+0x028 NumberOfUnCommittedPages : 0xfd
+0x02c NumberOfUnCommittedRanges : 1
+0x030 UnCommittedRanges : 0x001a0588 _HEAP_UNCOMMMTTED_RANGE
+0x034 AllocatorBackTraceIndex : 0
+0x036 Reserved : 0
+0x038 LastEntryInSegment : 0x001a1e88 _HEAP_ENTRY
HEAP_ENTRY
结构
0:001> dt -v _HEAP_ENTRY
struct _HEAP_ENTRY, 7 elements, 0x8 bytes
+0x000 Size : Uint2B //堆块大小,单位是堆的分配粒度
+0x002 PreviousSize : Uint2B //前一个堆块的大小
+0x000 SubSegmentCode : Ptr32 to Void //
+0x004 SmallTagIndex : UChar
+0x005 Flags : UChar
+0x006 UnusedBytes : UChar //因补齐多分配的字节
+0x007 SegmentIndex : UChar //堆块所在segment序号
堆块的分配和释放过程
书中659页的实验例子非常好,理解了就好。
0:001> !heap 1a0000 -hf
Index Address Name Debugging options enabled
1: 001a0000
Segment at 001a0000 to 002a0000 (00003000 bytes committed)
Flags: 00000002
ForceFlags: 00000000
Granularity: 8 bytes
Segment Reserve: 00100000
Segment Commit: 00002000
DeCommit Block Thres: 00000200
DeCommit Total Thres: 00002000
Total Free Size: 0000022f
Max. Allocation Size: 7ffdefff
Lock Variable at: 001a0608
Next TagIndex: 0000
Maximum TagIndex: 0000
Tag Entries: 00000000
PsuedoTag Entries: 00000000
Virtual Alloc List: 001a0050
UCR FreeList: 001a0598
FreeList Usage: 00000000 00000000 00000000 00000000
FreeList[ 00 ] at 001a0178: 001a1e90 . 001a1e90
001a1e88: 01808 . 01178 [10] - free
Heap entries for Segment00 in Heap 001a0000
001a0640: 00640 . 00040 [01] - busy (40)
001a0680: 00040 . 01808 [01] - busy (1800)
001a1e88: 01808 . 01178 [10]
001a3000: 000fd000 - uncommitted bytes.
堆调试支持
堆调试的选项通过windbg目录下的gflags.exe设置,有命令行版,也有GUI版。默认是开启一下3个选项。
0:001> !gflag
Current NtGlobalFlag contents: 0x00000070
htc - Enable heap tail checking
hfc - Enable heap free checking
hpc - Enable heap parameter checking