堆结构简介
首先我们需要认识两个重要的空闲堆块管理结构:空表(freelist)和快表(LookAside),在下图中可以看到,空表项位堆结构固定偏移0x178处,而快表指针则位于固定偏移0x580处

有一点需要注意,空表只会出现在可扩展的堆中,比如
hp = HeapCreate(0,0x1000,0x10000); //固定了初始大小0x1000和最大堆大小0x10000,堆无法扩展,空表始终为null
hp = HeapCreate(0,0x1000,0x10000); //未设置初始大小和最大大小,堆可动态扩展,存在空表项
空表
空表也就是Freelists,在上图中我们可以看到,空表的表项共有128,又 1024=128*8。由此可以引出空表存储空闲堆块的方式:特殊的第0项用于存储>=1024byte的空闲堆块,而后续表项从free[1]开始依次按8字节递增所存储的堆块大小。

List_entry是一个双向链表结构,因此我们的Freelist的实际存储方式如图(空闲时Flink与Blink均指向自身)

此处我们可以通过动态调试的数据进行验证(PS:此时堆段基址为0x360000)

快速单向链表
快表即Lookaside,同样通过堆结构体分析,我们可以得知定位快表地址的方式:FrontEndHeap指针位于0x580偏移处,指向快表

快表项远比空表项大,下图是快表项具体结构,每个表项占0x30字节,但我们目前只需要关注首部的堆块地址即可

进入这个Union中,看一下地址的具体存储方式:位于0x0处,并且以单链表形式存储地址

同理,快表也有128项,其存储的堆块大小排序方式与空表一致,但第0项未使用,如下图所示

接下来跳转到动态调试中的快表处进行验证,可以看到实际使用中即使是1字节的小堆块,也不会用到快表的0、1项,具体原因后文叙述

堆块结构
先前介绍了两个堆块的存储结构,接下来我们对堆块进行介绍
关于堆块分配,释放,合并的知识我们不详述,只需要知道,分配是从初始化的尾块分割,释放是将堆块放入上述两结构中,合并是将相邻的空闲堆块合成更大的堆块就可以
PS:在进行堆块操作中,始终以8byte为操作单位,并且若用户未请求,unused字节不会被初始化。
堆块有着空闲态和占用态两种情况,分别对应着不同的数据结构:
占用态
占用态堆块是不在快表和空表上的,其指针直接由用户进程保留;
堆块均有堆首,占8字节,用户申请的堆块则只对应堆块的数据区,即使只申请1byte堆空间,堆首的selfsize也是0x2(8byte堆首+8byte对齐单位)
关于堆首数据的详解,如下图所示

空闲态
空闲态堆块则是在占用态堆首的前提下,借用数据区前8byte,存储了一个_LIST_ENTRY结构,这也是空闲堆块索引的关键所在

注意,快表是不会被合并的,就是因为快表中的堆块结构,其堆首中的Flag均是0x1(busy)状态