当前位置:网站首页>堆(heap)系列_0x04:堆的内部结构(_HEAP=_HEAP_SEGMENT+_HEAP_ENTRY)
堆(heap)系列_0x04:堆的内部结构(_HEAP=_HEAP_SEGMENT+_HEAP_ENTRY)
2022-08-09 14:59:00 【可乐松子】
提示1:正常情况下,程序员是可以不用关心操作系统是怎么维护堆的;但是当堆被破坏,想要查找原因时,排查手段中gflags.exe启用相关堆调试支持的功能时,如果不理解堆的内部结构,估计会一脸懵逼…
提示2:查看堆的内部结构,最好会使用WinDbg,如果不会也没关系,下面我已经添加了查询结果的详细解释
说明:下面都是先给出结构体说明,后给出截图的方式;如果想深入研究页堆等更复杂的结构,一定要对堆的3种基础结构很了解才行
1.整体介绍
堆管理器通过Windows虚拟内存管理器来得到一块内存,这块内存被成为堆段(heap segment)
堆段的基本布局示意图

下面是一个堆段的使用顺序:
- 1.堆段最初被创建时
其中大部分虚拟内存是被保留状态(reserve),只有低地址的一小部分是被提交的(commit)状态
- 2.程序员申请内存
当堆管理器使用完已经提交的内存或不足以提供程序员所需内存,堆段将进一步提交(commit)一部分内存,然后对新的内存进行分割处理,将合适堆块给程序员使用
- 3.堆段使用完所有空间
堆管理器将创建一个新的堆段(heap segment),默认创建新堆段大小是之前的2倍(详细要看算法);会用一个链表统一记录相关的堆段(heap segment)
问题:堆管理器是否会释放某个堆段的内存?
不会,内存会始终保留,但是堆管理器可以取消内存提交(
decommit)
简要介绍一下每个堆都要有的3种数据结构,先看一下结构的层次关系
0:000> dt -r2 ntdll!_HEAP 00a30000 #00a30000为HeapCreate创建堆的句柄(指向堆管理结构的指针)
+0x000 Segment : _HEAP_SEGMENT #描述段的信息,0号段的首地址
+0x000 Entry : _HEAP_ENTRY #描述堆块的信息
#可以看到_HEAP包含_HEAP_SEGMENT和_HEAP_ENTRY,且3个结构的第一个描述的首地址相同
下面是WinDbg的截图

基本结论:每个堆至少拥有一个00号段(Segment),最多可拥有64个段;堆管理器在创建堆时会建立一个段,如果堆是可以增长的话,用完一个段后堆会自动增长;00号段开始处存放着一个HEAP结构的头部信息,每个段都有一个HEAP_SEGMENT结构来描述自己(在每个段的起始处)
2.HEAP结构
堆管理器使用
HEAP结构记录和维护每个堆的管理信息,位于每个堆的开始处
HeapCreate返回的句柄是创建堆的起始地址,即也是指向HEAP结构的指针
#1.ntdll!_HEAP大小
0:000> ?? sizeof(ntdll!_HEAP)
unsigned int 0x258(on600)
#2.列出当前进程的所有堆
0:000> !heap -h
Index Address Name Debugging options enabled
1: 00a90000 #句柄
Segment at 00a90000 to 00b8f000 (00007000 bytes committed)
2: 006b0000
Segment at 006b0000 to 006bf000 (00003000 bytes committed)
...
#3.查看具体一个堆的信息
0:000> dt ntdll!_HEAP 00a30000 #00a30000是堆的句柄
+0x000 Segment : _HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY #用于存放管理结构的堆块结构
+0x008 SegmentSignature : 0xffeeffee #签名,固定值,用来判断是段堆还是NT堆?
+0x00c SegmentFlags : 2
+0x05c VirtualMemoryThreshold : 0xfe00 #最大堆块大小(单位是分配粒度)
+0x060 Signature : 0xeeffeeff
+0x064 SegmentReserve : 0x100000 #段的保留空间大小
+0x068 SegmentCommit : 0x2000 #段的每次提交的大小
+0x06c DeCommitFreeBlockThreshold : 0x200 #解除提交的单块阈值(以分配粒度为单位)
+0x070 DeCommitTotalFreeThreshold : 0x2000
+0x074 TotalFreeSize : 0x38a #空闲块总大小(以分配粒度为单位)
+0x078 MaximumAllocationSize : 0x7ffdefff #可分配的最大值
+0x07c ProcessHeapsListIndex : 1 #本堆在进程堆列表种的索引
+0x0a4 SegmentList : _LIST_ENTRY [ 0xa30010 - 0xa30010 ] #记录堆中包含的所有段
+0x0b0 NonDedicatedListLength : 1 #堆块的数量,分析变长空闲链表[0]有用
+0x0c0 FreeLists : _LIST_ENTRY [ 0xa34408 - 0xa353f0 ] #记录空闲堆块链表的表头
+0x09c VirtualAllocdBlocks : _LIST_ENTRY [ 0xa3009c - 0xa3009c ]
+0x250 TuningParameters : _HEAP_TUNING_PARAMETERS
#说明1:VirtualMemoryThreshold : 0xfe00
#表示:可在段中分配的堆块最大值,单位是分配粒度(一般是8bytes),以用户数据区大小来衡量的,0xfe00*8=0x7f000=508KB=504+4(保留区),
#意味着堆中最大的普通堆块的用户数据区是508K,超过508K,直接调用ZwAllocateVirtualMemory分配内存,如果堆可以增长,可以直接分配512KB
#说明2:FreeLists,当有新的分配内存请求时,堆管理器会先在FreeLists链表里寻找最接近且满足要求的堆块;
# 找到,分配出去;没有找到,会考虑为这次请求内存提交新的内存也和建立新的堆块;释放时也负责回收
#说明3:VirtualAllocdBlocks
#如果申请内存大于虚拟内存的阈值,将不会被包含在heap segment和free list中,而是直接从虚拟内存管理器中进行分配
通过HEAP结构,可以索引到HEAP_SEGMENT链表入口SegmentList,空闲链表入口FreeLists等信息

3.HEAP_SEGMENT结构
HEAP_SEGMENT结构用来描述堆中段的基本信息
可以简单的认为堆被拆分成了很多个段,当创建一个堆时系统为这个堆分配一个00号的段,当00号段的内存被用完,如果初始创建堆时指定的标志是HEAP_GROWABLE,则会在分配一个01号的段,依次类推
#1._HEAP_SEGMENT大小
0:000> ?? sizeof(ntdll!_HEAP_SEGMENT)
unsigned int 0x40
#2.查看第一个段的信息
0:000> dt _HEAP_SEGMENT 00a30000
ntdll!_HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY #段中存放每个堆块信息
+0x008 SegmentSignature : 0xffeeffee
+0x00c SegmentFlags : 2
+0x010 SegmentListEntry : _LIST_ENTRY [ 0xa300a4 - 0xa300a4 ]
+0x018 Heap : 0x00a30000 _HEAP #属于的堆
+0x01c BaseAddress : 0x00a30000 Void #段的基地址
+0x020 NumberOfPages : 0xff #段的内存页数
+0x024 FirstEntry : 0x00a304a8 _HEAP_ENTRY #第一个堆块,最有用的信息
+0x028 LastValidEntry : 0x00b2f000 _HEAP_ENTRY #堆块的边界
+0x02c NumberOfUnCommittedPages : 0xf8 #尚未提交的内存页数
+0x030 NumberOfUnCommittedRanges : 1
+0x034 SegmentAllocatorBackTraceIndex : 0
+0x036 Reserved : 0
+0x038 UCRSegmentList : _LIST_ENTRY [ 0xa36ff0 - 0xa36ff0 ]
#FirstEntry: 0x00a304a8,FirstEntry用来指向第一个用户堆
WinDbg查看截图

4.HEAP_ENTRY结构
HEAP_ENTRY结构:描述每个堆块,里面包含的信息才是new、malloc返回的内存的地址涉及到的地方
HEAP_ENTRY结构示意图如下

WinDbg查看,_HEAP_ENTRY里面定义了一个union类型,因此看着结构会复杂一点

#1._HEAP_ENTRY长度
0:000> ?? sizeof(_HEAP_ENTRY)
unsigned int 8
#2._HEAP_SEGMENT中FirstEntry: 0x00a304a8,FirstEntry用来指向第一个用户堆
0:000> dt _HEAP_ENTRY 0x00a304a8
ntdll!_HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 Size : 0xf884 #堆块的大小(以分配粒度为单位)
+0x002 Flags : 0xdf '' #堆块的状态
+0x003 SmallTagIndex : 0x2e '.' #用于检查栈溢出的cookie
+0x000 SubSegmentCode : 0x2edff884
+0x004 PreviousSize : 0xf4d3 #前一个堆块的大小
+0x006 SegmentOffset : 0 ''
+0x006 LFHFlags : 0 ''
+0x007 UnusedBytes : 0x1 '' #因为内存对齐而多分配的字节数
#说明:Flags:01(busy)、02(存在额外描述)、03(使用固定模式填充)...
5.参考
- 1.《软件调试》第二版,卷2的第23章
- 2.《Windows高级调试》,第6章
- 3.《深入解析Windows操作系统》第七版,第五章
- 4.《Windows编程调试技术内幕》
边栏推荐
猜你喜欢

【工具使用】Keil5软件使用-基础使用篇
![[Deep learning] attention mechanism](/img/ef/108e6117546dc7fa4b634cd45c3fad.jpg)
[Deep learning] attention mechanism

MNIST数据集的训练(内附完整代码及其注释)

【力扣】98. 验证二叉搜索树

抱抱脸(hugging face)教程-中文翻译-分享一个模型

Stetman读paper小记:Backdoor Learning: A Survey(Yiming Li, Yong Jiang, Zhifeng Li, Shu-Tao Xia)

数据缺失对任务影响

Stetman的读paper小记:Deep Learning Backdoor Survey (Shaofeng Li, Shiqing Ma, Minhui Xue)

Vim实用技巧_8.替换(substitute)和global命令

【知识分享】Modbus通信协议详解
随机推荐
抱抱脸(hugging face)教程-中文翻译-文本分类
【力扣】207. 课程表
【Postgraduate Work Weekly】(Week 7)
NiN(Network in Network) pytorch实现
Stetman读paper小记:Backdoor Learning: A Survey(Yiming Li, Yong Jiang, Zhifeng Li, Shu-Tao Xia)
UDP 基本通信框架
抱抱脸(hugging face)教程-中文翻译-模型概要
hugging face tutorial - Chinese translation - tokenizers using Tokenizers
【研究生工作周报】(第八周)
【工具使用】Modbus Poll软件使用详解
嵌入式三级笔记
人脸识别示例代码解析(一)——程序参数解析
Stetman读peper小记:Defense-Resistant Backdoor Attacks Against DeepNeural Networks in Outsourced Cloud
【深度学习】SVM解决线性不可分情况(八)
理解泛型之得到泛型类型
【SQL】595. 大的国家
【研究生工作周报】(第三周)
研究生工作周报(第四周)
AlexNet pytorch实现
【研究生工作周报】(第十二周)