学习堆的一些笔记

以前做一些溢出题,基本上都是栈溢出,关于堆的比较少,而且,我自己觉得堆比较复杂,没有用心的去攻克它,现在决定好好的学习。

一、堆与栈的区别

(1)堆是一种在程序运行时动态分配的内存。动态就是指所需内存的大小在程序设计时不能预先决定,需要在程序运行是参考用户的反馈。

(2)堆在使用时需要程序员用专用函数进行申请,如 C 语言中的 malloc 等函数、C++中的 new 函数等都是最常见的分配对内存的函数。堆内存申请有可能成功,也有可能失败,这与申请内存的大小、机器性能和当前运行环境有关。

(3)一般用一个堆指针来使用申请得到的内存,读、写、释放都通过指针来完成。

(4)使用完毕后需要把堆指针给堆释放函数回收这片内存,否则会造成内存泄露。典型的释放函数包括 free、delete 等。

二、堆的数据结构与管理策略

堆块:出于性能的考虑,堆区的内存按不同大小组织成块,以堆块为单位进行标识,而不是传统的按字节标识。一个堆块包括两个部分:块首和块身。块首是一个堆块头部的几个字节,用来标识这个堆块自身的信息,例如,本块的大小、本块空闲还是占用等信息;块身是紧跟在块首后面的部分,也是最终分配给用户使用的数据区。

堆表:堆表一般位于堆区的起始位置,用于索引堆区中所有堆块的重要信息,包括堆块的位置、堆块的大小、空闲还是占用等。堆表的数据结构决定了整个堆区的组织方式,是快速检索空闲块、保证堆分配效率的关键。

在Windows中,占用态的堆块被使用它的程序索引,而堆表只索引所有空闲态的堆块。其中,最重要的堆表有两种:空闲双向链表,Lookaside(快表)

在说例子之前,查看 Advanced Windows Debugging 里面的资料,来全面解说堆的结构,来帮助了解。

Windows 内存管理策略

从上图可以看出,大多数的高层的内存管理器都使用了 Windows 堆管理器,而堆管理器又会使用虚拟内存管理器。

当进程启动的时,堆管理器会自动创建一个新的堆,这个就是默认的进程堆。尽管一些进程使用默认的进程堆,但是大多数的进程还是依赖CRT堆(通过 new/delete 和 malloc/free 等 API )去满足内存的需要。有些进程还会创建额外的堆(HeapCreate等申请)以将进程中不同的组件独立开来。

Windows 的堆管理器进一步划分,分为:

(1)Front End Allocator

Front End Allocator 又包括:

■ Look aside list (LAL) front end allocator

■ Low fragmentation (LF) front end allocator

Windows 的所有版本中除了(Vista 不清楚 win7 和 win8)外,都默认使用 LAL 分配器;而 Vista 则默认使用 LF 分配器。

(2)Back End Allocator

包含了一张空闲链表数组

1. 空表

空闲堆块的块首中包含一对重要的指针,这对重要的指针,这对指针用于将空闲堆块组织成双链表。按照堆块的大小不同,空表总共被分为128条。

堆区一开始的堆表区中有一个128项的指针数组,被称做空表索引。该数组的每一项包括两个指针,用于标识一条空表。

把0day的上面的图拿上来,可以看出,空表索引的第二项(free[1])标识了堆中所有大小为8字节的空闲堆块,之后每个索引项指示的空闲堆块递增8字节,例如,free[2]标识大小为16字节的空闲堆块,free[3]标识大小为24字节的空闲堆块,free[127]标识大小为1016字节的空闲堆块。因此有:

空闲堆块的大小=索引项(ID)x 8(字节)

把空闲堆块按照大小的不同链入不同的空表,可以方便堆管理系统高效检索指定大小的空闲堆块。需要注意的是,空表索引的第一项(free[0])所标识的空表相对比较特殊。这条双向链表链入了所有大于等于1024的堆块(小于512KB)。这些堆块按照各自的大小在零号空链表中依次升序的排列下去。

空说没什么意思,我们来实践看看,到底是不是如此。

同样还是 0day 那本书上的第一个堆的程序,在

hp = HeapCreate(0,0x1000,0x10000);
__asm int 3


HeapCreate之后,我们看到 EAX 里面的值是 0x00520000 查看 0x00520000 的内存空间的数据

0day那本书上没有说清楚堆的结构,由于好奇心,想弄清楚堆除了 堆块和堆表还有哪些结构,也就是说要了解堆的数据结构的定义,查阅相关的资料,说 windbg 可以看,输入命令: 

0:000> dt _HEAP 003a0000
ntdll!_HEAP
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : 0xeeffeeff
   +0x00c Flags            : 0x1000
   +0x010 ForceFlags       : 0
   +0x014 VirtualMemoryThreshold : 0xfe00
   +0x018 SegmentReserve   : 0x100000
   +0x01c SegmentCommit    : 0x2000
   +0x020 DeCommitFreeBlockThreshold : 0x200
   +0x024 DeCommitTotalFreeThreshold : 0x2000
   +0x028 TotalFreeSize    : 0x130
   +0x02c MaximumAllocationSize : 0x7ffdefff
   +0x030 ProcessHeapsListIndex : 5
   +0x032 HeaderValidateLength : 0x608
   +0x034 HeaderValidateCopy : (null) 
   +0x038 NextAvailableTagIndex : 0
   +0x03a MaximumTagIndex  : 0
   +0x03c TagEntries       : (null) 
   +0x040 UCRSegments      : (null) 
   +0x044 UnusedUnCommittedRanges : 0x003a0598 _HEAP_UNCOMMMTTED_RANGE
   +0x048 AlignRound       : 0xf
   +0x04c AlignMask        : 0xfffffff8
   +0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x3a0050 - 0x3a0050 ]
   +0x058 Segments         : [64] 0x003a0640 _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 [ 0x3a0688 - 0x3a0688 ]
   +0x578 LockVariable     : 0x003a0608 _HEAP_LOCK
   +0x57c CommitRoutine    : (null) 
   +0x580 FrontEndHeap     : (null) 
   +0x584 FrontHeapLockCount : 0
   +0x586 FrontEndHeapType : 0 ''
   +0x587 LastSegmentIndex : 0 ''
上面是在xp3 上运行的同一个程序,只是申请的基址变成了 0x003a0000 不过偏移量是不会变的。

查看 free[0] 的前向和后向指针

0:000> dt _LIST_ENTRY 0x003a0000+0x178
ntdll!_LIST_ENTRY
 [ 0x3a0688 - 0x3a0688 ]
   +0x000 Flink            : 0x003a0688 _LIST_ENTRY [ 0x3a0178 - 0x3a0178 ]
   +0x004 Blink            : 0x003a0688 _LIST_ENTRY [ 0x3a0178 - 0x3a0178 ]
都指向 0x003a0688 说明 HeapCreate 之后只有一个大于等于1024字节的堆块。

在 windows 2000 上 用 Ollydbg 查看,看偏移量是否都一样,即 0x00520688


猜测没错,也可以从上图看出,开始的时候,freeList 堆表中只有 free[0] 有数据,其它的都是指向自身。

查看 free[0] 所指向的地址段

0:000> dt _HEAP_ENTRY 0x003a0688-0x8
ntdll!_HEAP_ENTRY
   +0x000 Size             : 0x130
   +0x002 PreviousSize     : 8
   +0x000 SubSegmentCode   : 0x00080130 
   +0x004 SmallTagIndex    : 0 ''
   +0x005 Flags            : 0x10 ''
   +0x006 UnusedBytes      : 0 ''
   +0x007 SegmentIndex     : 0 ''

对应着上面的表,win 2000 用 OD 查看:


CurrentSize Block Size :0x0130   

Previous Block Size:0x0008 

SmallTagIndex:0x00

Flags:0x10

因为堆块是以8字节为单位的,所以大小为 0x130 X 8 = 0x980 个字节,加上基址,应该到了0x00521000 查看是否正确,确实没错。



2. 快表(Look aside list (LAL))


快表是Windows 用来加速堆块分配而采用的一种堆表。快表也有128条,组织结构与空表,只是其中的堆块按照单链表组织。快表总是被初始化为空,而且每条快表最多只有4个节点,故很快就会被填满。

这里有个小问题,0day 和 Advanced Windows Debugging 中有个不同的地方:


这。。。。。

其实我的理解是因为块首包括size,flags,use等状态标识,所以块首占8个字节,其它的是存储数据区,所以lookaside[1] 应该用不到。

俗话说:事实胜于雄辩,我们做实验来看看:

利用HeapCreate(0,0,0)创建一个可扩展的堆。查看堆信息

0:000> dt _HEAP 003a0000
ntdll!_HEAP
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : 0xeeffeeff
   +0x00c Flags            : 0x1002
   +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 : 5
   +0x032 HeaderValidateLength : 0x608
   +0x034 HeaderValidateCopy : (null) 
   +0x038 NextAvailableTagIndex : 0
   +0x03a MaximumTagIndex  : 0
   +0x03c TagEntries       : (null) 
   +0x040 UCRSegments      : (null) 
   +0x044 UnusedUnCommittedRanges : 0x003a0598 _HEAP_UNCOMMMTTED_RANGE
   +0x048 AlignRound       : 0xf
   +0x04c AlignMask        : 0xfffffff8
   +0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x3a0050 - 0x3a0050 ]
   +0x058 Segments         : [64] 0x003a0640 _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 [ 0x3a1e90 - 0x3a1e90 ]
   +0x578 LockVariable     : 0x003a0608 _HEAP_LOCK
   +0x57c CommitRoutine    : (null) 
   +0x580 FrontEndHeap     : 0x003a0688 
   +0x584 FrontHeapLockCount : 0
   +0x586 FrontEndHeapType : 0x1 ''
   +0x587 LastSegmentIndex : 0 ''


这次全用 windbg 查看内存,避免老换地址。这里没有讨论段表,


0:000> dt _HEAP_ENTRY 0x003a0680
ntdll!_HEAP_ENTRY
   +0x000 Size             : 0x301
   +0x002 PreviousSize     : 8
   +0x000 SubSegmentCode   : 0x00080301 
   +0x004 SmallTagIndex    : 0xfd ''
   +0x005 Flags            : 0x1 ''
   +0x006 UnusedBytes      : 0x8 ''
   +0x007 SegmentIndex     : 0 ''
0:000> dt _HEAP_ENTRY 0x003a0680+(0x301*8)
ntdll!_HEAP_ENTRY
   +0x000 Size             : 2
   +0x002 PreviousSize     : 0x301
   +0x000 SubSegmentCode   : 0x03010002 
   +0x004 SmallTagIndex    : 0xfc ''
   +0x005 Flags            : 0x1 ''
   +0x006 UnusedBytes      : 0x8 ''
   +0x007 SegmentIndex     : 0 ''

之前没说堆段的作用,即偏移量为 0x058 的块的作用:

堆管理器通过 Windows 虚拟内存管理器取得一大块内存。然后这块内存将分为不同大小的块,来满足程序内存的需要。当这块内存耗尽时,堆管理器将又向 Windows 虚拟内存管理器请求分配内存块,也成为堆段(Heap Segments)。

0:000> dt _HEAP_SEGMENT 0x003a0640
ntdll!_HEAP_SEGMENT
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : 0xffeeffee
   +0x00c Flags            : 0
   +0x010 Heap             : 0x003a0000 _HEAP
   +0x014 LargestUnCommittedRange : 0x3d000
   +0x018 BaseAddress      : 0x003a0000 
   +0x01c NumberOfPages    : 0x40
   +0x020 FirstEntry       : 0x003a0680 _HEAP_ENTRY
   +0x024 LastValidEntry   : 0x003e0000 _HEAP_ENTRY
   +0x028 NumberOfUnCommittedPages : 0x3d
   +0x02c NumberOfUnCommittedRanges : 1
   +0x030 UnCommittedRanges : 0x003a0588 _HEAP_UNCOMMMTTED_RANGE
   +0x034 AllocatorBackTraceIndex : 0
   +0x036 Reserved         : 0
   +0x038 LastEntryInSegment : 0x003a1e88 _HEAP_ENTRY


When allocating a block of memory:


1. The heap manager first consults the front end allocator’s LAL to see if a free block of memory is available; if it is, the heap manager returns it to the caller.
Otherwise, step 2 is necessary.

堆管理器会首先进行Look aside 进行匹配,否则进行第2步匹配


2. The back end allocator’s free lists are consulted:

a. If an exact size match is found, the flags are updated to indicate that the block is busy; the block is then removed from the free list and returned to the caller.

b. If an exact size match cannot be found, the heap manager checks to see if a larger block can be split into two smaller blocks that satisfy the requested allocation size. If it can, the block is split. One block has the flags updated to a busy state and is returned to the caller.The other block has its flags set to a free state and is added to the free lists. The original block is also removed from the free list.

后置分配器的空表将处理:

a. 如果寻找到大小刚好合适的空闲堆块,堆管理器修改其flag 为占用态,之后将此块从空表中移除

b.如果没找到大小匹配的堆块,堆管理器会检查一个大的堆分成两个小堆,是否满足需要的分配的大小,如果行就会分配。另外那块的flags 将设置为空闲状态并加入空表。原来的块同样会移除。


3. If the free lists cannot satisfy the allocation request, the heap manager commits more memory from the heap segment, creates a new block in the committed range (flags set to busy state), and returns the block to the caller.

如果空表不能满足其分配的需要,堆管理器从堆段中将提去更多的内存,创建一个新的块在提交的范围之内,然后返回给调用者。


When freeing a block of memory:

释放内存的时候


1. The front end allocator is consulted first to see if it can handle the free block.If the free block is not handled by the front end allocator step 2 is necessary.

优先链入Look Aside (只能链入四个空闲块),如果满了,则交给空表处理


2. The heap manager checks if there are any adjacent free blocks; if so, it coalesces the blocks into one large block by doing the following:
a. The two adjacent free blocks are removed from the free lists.
b. The new large block is added to the free list or look aside list.
c. The flags field for the new large block is updated to indicate that itis free.

堆管理器会检查是否有相邻的空闲块,如果有,合并成大的堆块将做以下操作:

a.两个相邻的将从空链表中移除

b.新的大块将加入空表和快表

c.这个堆块的标志将设为空闲
3. If no coalescing can be performed, the block is moved into the free list or look aside list, and the flags are updated to a free state.

如果不能进行合并的操作,这个块将移到空表或者快表,标志改成空闲状态


做个简单的实验,加深印象。还是拿 0day安全那本上的例子。

#include <windows.h>
main()
{
	HLOCAL h1, h2,h3,h4,h5,h6;
	HANDLE hp;
	hp = HeapCreate(0,0x1000,0x10000);
	h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
	h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
	h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
	h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
	h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
	h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

	_asm int 3	//used to break the process
	//free the odd blocks to prevent coalesing
	HeapFree(hp,0,h1); 
	HeapFree(hp,0,h3); 
	HeapFree(hp,0,h5); //now freelist[2] got 3 entries
	
	//will allocate from freelist[2] which means unlink the last entry (h5)
	h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8); 
		
	return 0;
}


在 Windows 2000上,VC 6.0, release 版本,运行后释放h1, h3,h5 后查看内存数据



目前的 FreeList[2] 的图我画一下



将 0x3606C8的前置指针改为0x41414141,再次申请一个8字节的空间,程序报错。



仅仅是一些笔记,大牛勿喷,本人小菜一个


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值