提示:下面几个问题都是关于堆的,如果你非常清楚,可以直接跳过不用看下面的内容了
- 1.堆和栈有什么区别?(网上八股文很多,百度"内存四分区"就有介绍,但是最好不要停在表面知识)
- 2.写程序时用
new/malloc
是在堆还是栈上分配的内存?free
只传一个指针,没有传大小,它是怎么释放内存的?(侯捷的C++内存管理课程有详细的讲解)- 3.NT全局标志、
gflags
都是用来做什么的?(当堆被破坏时,想要启动页堆的调试支持,就一定绕不开全局标志)- 4.页堆、NT堆、CRT堆、默认堆、私有堆,这些术语都是什么意思?(使用应用程序验证器,
malloc
、Heap
开头的API、进程创建堆、堆破坏想立刻捕捉到都要涉及到这些堆的使用)
下面将堆的相关内容整理成几篇笔记,先对堆有一个简单的认识吧
1.堆和栈区别
堆(heap
)是在运行期间
动态申请内存空间的一种方法,平时使用的malloc
、new
都是在堆上分配内存
提示1:提到堆,就一定要会想到另一个术语:栈(
stack
),简单区别如下:
说明 栈(stack)
堆(heap)
分配和释放内存 编译器在 编译阶段
自动产生相关指令,在程序的默认栈上预留空间程序员自己处理,且分配和释放要严格匹配 生存期 1.线程的创建就会为其创建栈空间,一般大小为1MB,因此不适合分配运行时才决定大小的缓冲区
2.栈帧会随着函数调用和返回而创建和销毁,因此栈上变量只在函数内有效进程存在期间一直有效
提示2:堆和栈是2个不相关的内容,一定不要混在一起认为是一回事,也不知道是谁是第一个将
堆栈
翻译在一起的人…
下面是内存和变量使用的分区图(网上的图片,找不到出处了,里面有堆和栈的区别)
- heap
是向上
(高地址)增长的;new
和malloc
都是在heap上申请的内存,这部分内存的生命周期是与程序相同的
深入学习:要学习堆块结构、
gfalgs
工具、页堆相关知识
- stack
是向下
(低地址)增长的,主要用途是暂时保存函数的局部变量、传递参数和保存函数的返回地址
深入学习:要学习栈帧的相关知识
提示:IDA中通过变量名就可以区分出是参数还是局部变量,在一个函数内部,
ebp-xx
的形式基本就是局部变量,ebp+xx
的形式基本就是参数
2.堆的框架
下图是windows内存架构示意图,先来了解一些术语
- 内存管理器(
Memory Manager
)
将一块大的内存委托给堆管理器,系统提供堆内存的源头
- 堆管理器(
Heap Manager
)
问题1:堆管理器存在意义?
堆管理器会将大块内存分割成不同的小块内存给应用程序使用,这样内存管理器只要负责处理大规模的内存分配就可以;其实学习堆,很大一部分时间是在学习堆管理器
问题2:堆管理器的实现?
有很多种实现堆管理器的方式,windows操作系统的实现主要在2个地方:ntdll.dll
(子系统的API调用ntdll.dll
中函数)和ntoskrnl.exe
(驱动程序调用的函数在这个exe中)
ntdll.dll
中实现了一个通用的堆管理器,目的是给用户态进程提供内存服务,通常称为Win32管理器
;其中的SDK提供了一些API(HeapAlloc
、HeapFree
等)可以访问堆管理器,这些前缀是Heap
的API接口实际上是ntdll.dll
中函数的包装而已
- ntdll.dll中部分堆管理函数
//WinDbg查看部分堆管理函数(节选)
0:000> dt ntdll!*Heap*
7708b890 ntdll!RtlSizeHeap //获取堆块大小
77070fa0 ntdll!RtlCreateHeap //创建堆
7705f8c0 ntdll!RtlDestroyHeap
77073bd0 ntdll!RtlFreeHeap //释放堆
77075da0 ntdll!RtlAllocateHeap //在堆上分配内存
- windows常见的堆函数
HeapCreate/HeapDestory //创建/删除堆
HeapAlloc/HeapFree/HeapReAlloc //分配/释放堆块、更改堆块大小
HeapLock/HeapUnLock //控制堆操作互斥
HeapWalk //枚举堆中的项和区域
下面是一些常用堆介绍,最好知道每种堆的用途和原理;由于页堆比较复杂,这篇笔记是对堆的介绍,因此暂时不介绍复杂的堆类型了
3.堆的类型
type 1:CRT堆
为了支持C的内存分配函数(malloc/free
)和c++的运算符(new/delete
)要求,编译器的C运行时库创建了一个专门的堆用来给这些函数使用;堆句柄会存储在msvcrt
的全局变量_crtheap
中
根据分配堆块的方式不同,会有3种工作模式;当创建堆时,会选择一种模式
模式 | 说明 |
---|---|
SBH(Small Block Heap)、旧SBH | CRT堆会使用虚拟内存分配API从内存管理器中申请一块大内存,再分割成小的堆给应用程序使用 |
系统模式 | 只是将堆块分配请求转发给它基于的Win32堆 |
type 2:NT堆
简单理解是ntdll.dll
实现的堆管理器提供的堆的一种类型,下面是用户模式下的NT堆分层示意图
其中:核心层提供了堆的基本功能,只有用户模式的堆可以在核心层上存在前端层
Windows10之前,只有一种堆类型(NT堆
),Windows10引入了段堆(segment heap)
的新类型;默认情况下,所有通用windows平台(UWP)和某些系统进程使用段堆,其他所进程使用NT堆,可通过注册表修改
扩展:UWP至少包含3个堆:1.默认堆、2.共享堆、3.CRT堆
type 3:段堆
下面是段堆类型的架构,根据申请的内存大小,使用不同的分配方式
NT堆和段堆对比:
- 段堆的元数据内存占用更小,更适合手机等小内存设备
- 段堆的元数据和实际数据是分隔的,NT是放在一起的
- 段堆只能用于可增长堆,且无法用于内存映射文件
段堆启动/禁止:
- 特定可执行文件:映像文件中的
FrontEndHeapDebugOptions
(4:禁用;8:启动) - 针对全局:这个暂时不用关心
示例:查看UWP进程的堆信息
windows10打开一个UWP应用(Calculator.exe),使用WinDbg附加Calculator进程
#1.显示堆的简要信息
0:025> !heap
Heap Address NT/Segment Heap
178ab040000 Segment Heap #默认堆
178aafd0000 NT Heap #与用户定义的堆块配合使用,因此创建的是NT堆
178aafe0000 Segment Heap
178ab1a0000 Segment Heap
178adff0000 Segment Heap
178af560000 NT Heap
178af580000 NT Heap
178af9e0000 NT Heap
#2.查看NT堆结构
0:025> dt ntdll!_HEAP 178aafd0000
+0x000 Segment : _HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x010 SegmentSignature : 0xffeeffee #签名
+0x014 SegmentFlags : 1
+0x198 FrontEndHeap : (null) #是否存在前端层?非null代表LFH前端层(前端层只有这一种)
+0x1a0 FrontHeapLockCount : 0
+0x1a2 FrontEndHeapType : 0 ''
#3.查看段堆结构
0:025> dt ntdll!_segment_heap 178ab040000
+0x000 EnvHandle : RTL_HP_ENV_HANDLE
+0x010 Signature : 0xddeeddee #签名
+0x014 GlobalFlags : 0
...
+0x280 VsContext : _HEAP_VS_CONTEXT #包含VS和LFH相关信息
+0x340 LfhContext : _HEAP_LFH_CONTEXT
#说明:NT堆和段堆的签名都是固定的,且相对于堆句柄的偏移量都是0x10,这使如RtlAllocateHeap
# 等函数通过堆句柄 + 偏移可以选择实现哪种堆
WinDbg命令:使用
!heap -s
可以了解每个堆的详细信息
type 4:默认堆和私有堆
相对一个进程来说,一般有2种堆:默认堆和私有堆;默认堆是进程创建时创建的,私有堆是程序员调用HeapCreate
创建的
问题:程序员怎么使用默认堆和私有堆?
通过句柄(堆的起始地址);默认堆的句柄保存在_PEB->ProcessHeap
字段中,程序里其他的堆句柄都是被依次放在一个数组中(在PEB
的ProcessHeaps
中存储)
#1.peb中heap相关信息
0:000> .process
Implicit process is now 0032f000 #默认堆的句柄
0:000> dt ntdll!_PEB 0032f000
+0x018 ProcessHeap : 0x008f0000 Void #进程(默认)堆句柄
+0x090 ProcessHeaps : 0x7756c760 -> 0x008f0000 Void #存堆句柄数组,首地址是0x7756c760
#2.查看堆句柄数组
#进程中堆的总数是7个,008f0000是默认堆的的句柄,每个句柄都指向一个类型为_HEAP的数据结构
0:000> dd 0x7765c760 l8
7765c760 008f0000 02480000 00710000 006c0000 #进程默认堆总是位于数组的第一项
7765c770 02930000 05510000 05660000 00000000
堆的Introduction就先介绍这么多吧,写太多看着就枯燥了
4.参考
- 1.《软件调试》第二版,卷2的第23章
- 2.《Windows高级调试》,第6章
- 3.《深入解析Windows操作系统》第七版,第五章
- 4.《Windows编程调试技术内幕》