在系统内部堆就是一块预定的地址空间区域。刚开始堆的大部分页面都没有调拨物理存储器。随着我们不断的从堆中分配内存,堆管理器会给堆调拨越来越多的物理存储器。这些物理存储器始终是从页交换文件中分配的。释放堆中的内存时,堆管理器会撤销已调拨的物理存储器。
进程初始化时,系统会在进程的地址空间中创建一个堆。这个堆被称为进程的默认堆。默认情况下,这个堆的地址空间区域大小是1MB。程序员可以控制这个大小。我们可以在创建应用程序时用/HEAP连接器开关来改变默认堆的大小。由于DLL没有与之关联的堆,因此在创建DLL时,不应该使用/HEAP开关。默认堆的句柄会被保存在进程环境块_PEB的ProcessHeap字段中。
通过.process获得_PEB的地址
ProcessHeap字段即为进程默认堆。其上的HeapSegmentReserve是进程堆的预订(默认为1MB)大小。HeapSegmentCommit是进程堆的初始提交大小,默认值为2个内存页大小(x86内存页为4KB),NumberOfHeaps字段用来介绍堆的总数。ProcessHeaps是一个数组,用来记录每个堆的句柄。
在windbg中可以使用!heap扩展显示堆使用信息,控制堆管理器中的断点,检测泄露的堆块,搜索堆块或者显示页堆(page heap)信息。
!heap -h列出当前进程的所有堆
这些都是进程分配的默认堆,除了进程的默认堆以外,程序也可以调用HeapCreate函数创建自己的堆,这样创建的堆被称为私有堆,3b0000就是私有堆。
!heap -a 3b0000显示堆的信息
Segment at 3b0000 to 3c0000 (00001000 bytes committed)指明堆的内存范围和提交字节数。Granularity: 8 bytes指明堆块分配粒度。在0号堆段的3b0680为空闲堆。
从_PEB中的堆数组可以知道,进程中可以存在多个堆。在每个堆内部又可以分为多个堆段,每个堆段又可以分为多个堆块。堆管理器在创建堆时创建的第一个段,我们将其称为0号段。堆是可增长的,当一个段不能满足要求时,堆管理器会继续创建其他段。但最多可以有64个段。每个堆使用_HEAP结构来描述,用命令dt _HEAP 3b0000来查看私有堆3b0000来查看堆结构。
_HEAP_ENTRY 用于存放堆管理数据结构的堆块结构
VirtualMemoryThreshold : 0xfe00 以分配粒度为单位的堆块阀值,表示可以在段中分配的堆块的最大有效(即应用程序可以实际使用的)值,该值为508kB
SegmentReserve 堆段保留大小,SegmentReserve字段的值为0x100000 = 1MB。表示我们请求创建的堆的最大大小。
SegmentCommit 堆段提交大小,SegmentCommit为0x2000,表示仅仅提交两个页面为8KB。
ProcessHeapsListIndex PEB中ProcessHeaps的索引
VirtualAllocdBlocks 虚拟内存分配块链表,超过堆块阀值将在此分配。当应用程序从堆中分配的堆块的最大大小大于堆块阀值,堆管理器会直接从内存管理器中分配,并不会从从空闲链表申请。同时将此空间添加到VirtualAllocdBlocks结构所指向的链表中。
Segments是一个数组,它记录着堆拥有的所有段。每个元素类型为_HEAP_SEGMENT结构。
FreeLists是一个双向链表的头指针,该链表记录着所有空闲堆块的地址。链表元素为FREE_LIST结构,该链表为双向链表,每个链表中都保存着一些空闲堆块。各个链表项都指向_HEAP_FREE_ENTRY结构中的FreeList字段。当应用程序申请新的空间时,堆管理器会首先遍历这个链表,如果找到满足需要的堆块就分配出去。否则便要考虑建立新的堆块或从内存管理器申请空间。在释放时,当不满足解除提交条件时,大多数情况下也是将要释放的堆块加入到该空闲链表中。Free_list数组,该数组有128个元素,用来存储各个空闲链表的表头。空闲链表的元素为_HEAP_FREE_LIST类型。
FrontEndHeap该字段为指针指向前端分配器
LastSegmentIndex表示堆中最后一个段的序号,加1便是总段数。
上面LastSegmentIndex为0说明只有一个段,在看下堆段的结构,每个段使用_HEAP_SEGMENT结构描述
Entry字段是一个数组,存储着该段所有的堆块。由于每个堆块使用_HEAP_ENTRY结构描述,因此该数组元素类型为_HEAP_ENTRY。
Heap字段维护该块块所属的堆的_HEAP结构的首地址。
BaseAddress字段维护该段的基地址。
FirstEntry表示该段中第一个堆块的地址。
LastEntryInSegment表示最后一个堆块。
现在执行HeapAlloc函数在3b0000申请一个堆块,再执行!heap -a 3b0000显示堆的信息
3b0680已在使用,也是我们刚申请的堆块,3b06a8成为了新的空闲堆,再执行dt _HEAP_SEGMENT 3b0640可以看到最后一个堆块的位置变成了3b06a8
可以看出段内部又可以分为多个堆块。堆块使用 _HEAP_ENTYR结构来描述,该结构占8 Byte。_HEAP_ENTRY结构之后就是供应用程序使用的区域。调用HeapAlloc函数将返回HEAP_ENTRY之后的地址。此地址减去8Byte便可以得到_HEAP_ENTRY结构。查看堆块结构
Size,堆块的大小,以分配粒度为单位 。前面知道粒度为8 bytes,且UnusedBytes为0x18,所以真正的大小为5*8-0x18=0x10字节
PreviousSize,前一个堆块的大小
Flags,标志
UnusedBytes,因为补齐而多分配的字节数
SegmentIndex,这个堆块所在堆段的序号
该结构只比_HEAP_ENTRY多了个FreeList字段,用来存储空闲链表的一个链表项,所以_HEAP_FREE_ENTRY大小为16字节。多个链表项构成一个空闲链表。因为该链表仅仅只有一个空闲堆块,因此上述_LIST_ENTRY的Flink和Blink 字段均指向空闲链表的头结点。
HeapAlloc申请的空间:
调用HeapFree释放后:
可以看出释放后修改了_HEAP_ENTRY的Flags字段,后8字节添加了FreeList链表。
FreeList链表中的3b01a0地址保存的值为3b0688,也是我们调用HeapFree释放空间的地址。
当再次调用HeapFree释放时,FreeList会将释放的地址加入链表
在进行堆溢出的时候,通常会覆盖_HEAP_ENTRY或者_HEAP_FREE_ENTRY结构,但一般程序中会进行多次的堆分配,所以大多都是覆盖_HEAP_ENTRY。