堆溢出利用
堆的工作原理
Windows堆的历史
微软操作系统堆管理机制的发展大致可以分为三个阶段(Win32):
- Windows 2000~Windows XP SP1:堆管理只考虑完成分配任务和性能因素,丝毫没有考虑安全因素
- Windows XP 2~Windows 2003:加入了安全因素,比如修改了块首的格式并加入安全cookie,双向链表结点在删除时会做指针验证等。
- Windows Visa~Windows 7:不论在堆分配效率上还是安全与稳定性上,都是堆管理算法的一个里程碑。
这里主要关注Windows 2000~Windows XP SP1。
堆与栈的区别
栈的特点:
- 在程序设计时已经规定好怎么使用,使用多少内存空间
- 在使用时不需要额外的申请操作,系统栈会根据函数中的变量声明自动在函数栈帧中给其预留
- 栈空间由系统维护,分配、回收都由系统来完成,最终达到栈平衡
堆的特点:
- 堆是一种在程序运行时动态分配的内存,需要在程序运行时参考用户的反馈
- 堆的使用需要程序员进行专门的申请,如:C——malloc函数、C++——new函数等,且申请可能失败
- 一般用一个堆指针来使用申请得到的内存,读、写、释放都通过这个指针完成
- 使用完毕后需将堆指针传给堆释放函数(free、delete)回收内存,否则会造成内存泄漏
堆内存 | 栈内存 | |
---|---|---|
典型用例 | 动态增长的链表等数据结构 | 函数局部数组 |
申请方式 | 需要函数申请,通过返回指针使用 | 在程序中直接声明即可 |
释放方式 | 需要将指针传给释放函数 | 函数返回时,系统自动回收 |
管理方式 | 需要管理员申请与释放 | 申请与释放均由系统自动完成,达到栈区平衡 |
所处位置 | 变化范围很大 0x0012XXXX | |
增长方向 | 由内存低地址向高地址排列(不考虑碎片等情况) | 由内存高地址向低地址增加 |
堆的数据结构与管理策略
现在操作系统的堆数据结构:
- 堆块
出于性能考虑,堆区内存按不同大小组织成块,以堆块为单位进行标识。- 块首:堆块头8个字节,用于标识堆块信息
- 块身:紧跟在块首后的部分,也是最终分配的数据区
空闲堆块结构:
占用堆块结构:
堆管理系统返回的指针一般指向块身的起始位置。
- 堆表
堆表一般位于堆区的起始位置,用于索引堆区中所有堆块的重要信息。
在Windows中,占用态的堆块被使用它的程序索引,堆表只索引所有空闲态的堆块,最重要的堆表包括两个:- 空闲双向链表Freelist(空表)
空闲堆块的块首中包含一堆重要的指针,用于将空闲堆块组织成双向链表,按照堆块的大小,空表总共分为128条:
- 快速单向链表Lookaside(快表)
快表是Windows用来加速堆块分配而采用的一种堆表,从来不会发生堆块合并(其中空闲块块首被设置为占用态,防止堆块合并)。
快表也128条,总是被初始化为空,且每条快表最多4
个结点,很快会被填满。
- 空闲双向链表Freelist(空表)
堆中的操作:
- 堆块分配
- 快表分配:寻找大小匹配的空闲堆块—>将其状态改为占用态—>将其从堆表中“卸下”—>返回一个指向堆块块身的指针
- 普通空表分配:首先寻找最优的空闲块分配,若失败寻找次优的
- 零号空表(free[0])分配:由于零号空表的空间表为升序的,故先查找最后一个是否满足,如果满足再正向搜索
- “找零钱”现象:当出现次优分配时,会先从大块中按请求“割”出一块,然后将剩下部分重新标注块首链入空表。由于快表只有在精确匹配时才会分配,故不存在“找钱”现象。
- 堆块释放
释放堆块为将堆块状态改为空闲,链入相应的堆表。 - 堆块合并
堆块合并由堆管理系统自动完成。
经过反复的申请与释放,堆区会产生很对内存碎片,会进行堆块合并操作:将两个相邻的块从空闲链表中“卸下”—>合并堆块—>调整合并后大块的块首信息—>重新链入空闲链表。
在具体进行堆块分配和释放时,根据操作内存的不同策略也不同:
分配 | 释放 | |
---|---|---|
小块(SIZE<1KB) | 快表分配(失败)—>普通空表分配(失败)—>堆缓存(heap cache)分配(失败)—>零号空表分配(失败)—>进行内存紧缩后再尝试分配(失败)—>返回NULL | 优先链入快表(失败)—>链入相应的空表 |
大块(1KB<=SIZE<512KB) | 堆缓存分配(失败)—>使用free[0]中的大块进行分配 | 优先放入堆缓存(失败)—>链入freelist[0] |
巨块(SIZE>=512KB) | 巨块非常罕见,用到虚分配方法 | 直接释放,无堆表操作 |
在堆中漫游
堆分配函数之间的调用关系
Windows平台下的堆管理架构:
所有的堆分配函数最终都将使用位于nydll.dl