代码
#include <windows.h>
int main()
{
HLOCAL h1,h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);//创建一个新的堆
_asm int 3//中断,进入OD调试
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);
h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);
h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);
h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
_asm int 3
HeapFree(hp,0,h1);
HeapFree(hp,0,h3);
HeapFree(hp,0,h5);
_asm int 3
HeapFree(hp,0,h4);
_asm int 3
return 0;
}
实验目的
简单理解空表
实验准备
环境:windows xp
编译器:vc++
调试器:OD
实验过程
1.把OD设成默认调试器,打开这段程序触发异常正好卡在HeapCreate后面,这时候的正好把新建好的堆的地址传入eax。
试了下Heap_Vis这个插件,好像不是那么好用,我用了下,直接给我卡死了,最后还是靠Ctrl+Alt+Del才救回来的(有可能是插件冲突了?懒得去搞了
2.找到堆的地址发现堆的结构前面先是一段段表。中间是虚分配表,因为堆才初始化,没有虚分配记录所以全都为NULL,然后就是32位的bitmap应该是来统计堆表的分配,如果表被占用则,该对应的bit位上为1.
然后接下来就是空表索引区,但是由于都没有被占用,所有的空表都指向自己的位置。
其中零号空表(0x003a0178)指向的是尾块。
PS:堆表表头的结构为
Byte | Name |
---|---|
1~2 | Self Size 自身的长度 |
3~4 | Previous chunk size 上一节的长度 |
5 | Segment Index 段索引 |
6 | Flags 该位为1的时候,表示该表被占用了 |
7 | Unused bytes |
8 | Tag index |
9~b | Flink in freelist (占用态) |
c~f | Blink in freelist (占用态) |
3.观察到零号空表指向的尾块表头,表头下面的就是指向零号空表的双向链表。实际上这个堆块开始于 0x0030680,一般引用堆块的指针都会跃过 8 字节的块首,直接指向数据区。Self Size为0x130,所以总空间为 0x130 * 8。
(1)堆块的大小包括了块首在内,即如果请求 32 字节,实际会分配的堆块为 40 字节:8字节块首+32 字节块身。
(2)堆块的单位是 8 字节,不足 8 字节的部分按 8 字节分配。
(3)初始状态下,快表和空表都为空,不存在精确分配。请求将使用“次优块”进行分配。这个“次优块”就是位于偏移 0x0680 处的尾块。
(4)由于次优分配的发生,分配函数会陆续从尾块中切走一些小块,并修改尾块块首中的size 信息,最后把 freelist[0]指向新的尾块位置。
3.把断点的位置移到HeapAlloc的后面观察每个空间的申请。
0x003a0680到0x003a06e0就是1到6号申请的空间。
开头是02,则代表分配了2 * 8个字节的空间。
开头是04,则代表分配了4 * 8个字节的空间。
然后这个时候再看尾块的表头的Self Size为0x120,因为前面分配了0x10的空间(2 * 4 + 4 * 2 = 0x10)
这时零表中指向尾块的指针也变成了0x003a0700
4.把断点的位置移到HeapFree h5的后面,观察h1和h3链入了Freelist[2],h5链入了Freelist[4]
5.又把断点的位置移到HeapFree之后观察堆表h1链入了Freelist[2],h3脱离了Freelist[2]与h1组成的链表链入了Freelist[8],同时观察h3开头的02已经变成了08,此时已经完成了h3,h4,h5的合并。h5也从Freelist[4]中取下了。
堆块合并的过程。堆块合并可以更加有效地利用内存,但往往需要修改多处指针。因此,堆块合并只发生在空表中。在强调分配效率的快表中,堆块合并一般会被禁止(通过设置堆块为占用态)。另外,空表中的第一个块不会向前合并,最后一个块不会向后合并。
观察此时Freelist[4]已经变为指向自身,Freelist[2]也只链入了一个h1
PS:调试堆好像不能用OD直接载入,必须的在进程中调试,不然堆的位置就会出现乱码。