Loading

堆调试

《软件调试读书笔记》第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
posted @ 2015-12-26 02:49  Lnju  阅读(526)  评论(0)    收藏  举报