调试堆内存
启用用户堆栈跟踪(User Stack Trace
)
- 方法一
- 通过
gflags.exe
为目标进程添加堆栈跟踪标志:
- 通过
- 方法二:
- 打开
Global Flags
,设置如图示:
- 打开
相关问题
- 在实践过程中,发现勾选
Enable page heap
之后,虽然点击上图的应用确认之后,查看注册表,值为0x02001070
,同时用gflags.exe
在cmd
里面打印的也是0x02001070
,看似正确:- 但实际上,在
windbg
里面,用!gflag
打印出来的值为0x02000000
- 经实践,如果不勾选
Enable page heap
只勾选上图左边四项,注册表值为0x00001070
,且用!gflag
打印出的值也是0x00001070
- 但实际上,在
验证UST
是否生效
- 方法一
Win + R
,输入regedit
打开注册表- 定位到
Image File Execution Options
路径:
若aet_breakpad_test.exe
的键不存在,说明没设置成功 - 检查
GlobalFlag
的值
UST
对应的标志值为0x1000
(即启用用户堆栈跟踪)
- 方法二
- 启动
aet_breakpad_test.exe
- 打开
windbg
附加到进程(或者直接用windbg
启动不需要附加) !gflag
:查看全局标志heap -p -a [堆块地址]
:检查堆分配调用栈
若分配堆块时能显示调用栈(如Alloc
函数的堆栈回溯),则UST
生效
- 启动
!gflag
概述
GlobalFlag
0x02000000
- 启用
hpa
(Heap Placement at Ends of Pages
)
- 启用
0x00001000
- 启用
ust
(User Stack Trace
)
- 启用
0x00000070
- 启用
Heap Tail Checking
- 启用
hpa
- 概述
- 页堆(
Page Heap
) 是Windows
堆管理器提供的调试功能,用于检测堆内存溢出、越界访问等问题 - 其核心原理是通过在堆块后附加 不可访问的栅栏页(
Guard Page
),当程序试图越界写入或读取时,会立即触发访问违规异常(Access Violation
),从而快速定位问题
- 页堆(
- 特性:
- 内存隔离:每个堆块独占一块内存区域,避免相邻堆块干扰
- 溢出检测:堆块尾部设置不可访问的栅栏页,溢出操作直接触发崩溃
- 精准定位:异常发生时,调试器可直接捕获到导致溢出的代码位置
ust
- 概述
- 用户态栈回溯(
User Stack Trace
,UST
)是一种在程序运行中记录函数调用链的技术,用于快速定位内存泄漏、越界访问等问题 - 通过
gflag
工具启用UST
后,堆管理器会在内存分配时记录调用栈信息,便于后续调试器(如WinDbg
)通过!heap -p -a
命令查看完整的函数调用路径
- 用户态栈回溯(
- 场景
- 内存泄漏排查:追踪未释放堆块的分配源头
- 越界访问调试:结合页堆(
Page Heap
)捕获越界操作的调用栈 - 多线程问题分析:识别线程竞争导致的资源未同步释放
htc
- 概述
- 堆尾检查(
Heap Tail Checking
,HTC
) 是Windows
堆管理器提供的一项调试功能,用于检测堆内存的尾部溢出问题
- 堆尾检查(
- 其核心原理是:
- 在堆块的尾部添加特定填充字节(通常为
0xABABABAB
),当程序尝试越界写入堆块尾部时,这些填充字节会被破坏,堆管理器在释放内存或主动检查时触发异常,帮助开发者快速定位溢出位置
- 在堆块的尾部添加特定填充字节(通常为
- 场景
- 越界写入检测:例如数组索引超出范围或字符串拷贝未截断
- 内存破坏分析:如全局变量覆盖堆块尾部数据
- 多线程同步问题:并发操作导致的堆块边界冲突
堆内存
操作系统级内存分配流程
- 当进程启动时,操作系统为其分配虚拟地址空间(
Virtual Address Space
),但此时仅建立虚拟页表,物理内存页并未实际分配 - 首次访问某个虚拟页时触发 页错误(
Page Fault
),操作系统才会通过VirtualAlloc
等API
提交物理内存页,并更新页表映射
new
的调用链路
C++
的new
操作符通过以下链路与操作系统交互:new → operator new → malloc → HeapAlloc → VirtualAlloc
(最终路径)
operator new
的默认实现- 默认情况下,
operator new
调用malloc
(如Microsoft CRT
的实现),而malloc
在 Windows 中底层调用HeapAlloc
- 默认情况下,
HeapAlloc
与VirtualAlloc
的协作HeapAlloc
管理进程的默认堆(Default Heap
),当堆内存不足时,会调用VirtualAlloc
向操作系统申请更大的内存块(通常以1MB
为单位),然后分割为小块供HeapAlloc
管理
new
与物理内存提交的延迟性
- 初次访问触发提交
- 即使
new
成功返回指针,对应的物理内存页可能尚未分配。例如: - 此时,
p[0]
的写入操作会触发页错误,操作系统通过VirtualAlloc
提交物理内存
- 即使
性能优化策略
- 预提交内存(
MEM_COMMIT
)- 通过
VirtualAlloc
的MEM_COMMIT
标志强制提交物理内存,避免首次访问的延迟: - 此方法常用于实时性要求高的场景(如高频交易系统)
- 通过
总结(记忆)
- 用
new
在堆上分配的内存,本质上是在进程的虚拟地址空间里面的堆区,去访问虚拟页,虽然可能成功获得了指针,但是对应的物理内存的页框可能并未分配- 用
new
在堆上分配内存时,C++
运行时库(如malloc
)会向操作系统的 堆管理器(如Windows
的HeapAlloc
或Linux
的brk
/mmap
)请求虚拟地址空间 - 操作系统仅在进程首次访问虚拟页时,才会通过 缺页中断(
Page Fault
) 分配物理页框
- 用
UMDH
(User-Mode Dump Heap
)
概述
- Windows 平台下用于检测用户态程序堆内存泄漏的专业工具,属于 Windbg 工具集的一部分
- 它通过记录程序运行过程中堆内存分配的调用栈信息,对比不同时间点的内存快照差异,定位未释放的内存块及其源头代码
工作原理
- 堆内存跟踪机制
UMDH
依赖于Windows
堆管理器的调试功能- 通过启用
gflags
工具的 用户态堆栈追踪数据库(User-Mode Stack Trace Database
),UMDH
可捕获每次内存分配(malloc
/new
)时的调用栈信息
- 快照对比分析
- 通过两次内存快照(
begin.log
和end.log
)的差异,UMDH
识别出在两次快照间新增且未释放的内存分配记录,并关联到具体的代码位置 - 例如,若某段代码在两次快照间分配了
100
次内存但未释放,UMDH
会标记该调用栈为潜在泄漏点
- 通过两次内存快照(
- 符号解析支持
- 需正确配置符号路径(
_NT_SYMBOL_PATH
),确保UMDH
能解析程序及系统库的调试符号(PDB
文件),将内存地址映射到源码行号
- 需正确配置符号路径(
UMDH
使用步骤
- 启用堆栈追踪
- 使用
gflags
为目标程序开启堆栈追踪: - 详细看上面的启用
UST
- 使用
- 配置符号路径
- 设置符号路径,包含程序 PDB 文件和微软符号服务器:
- 生成内存快照
- 对比快照差异
- 生成差异报告:
- 报告中将列出新增内存分配的调用栈及对应的源码位置
关于工具使用
- 需通过命令提示符(
CMD
)或PowerShell
执行相关命令
begin.log
end.log
diff.log
- 部分内容如下
+5c
:(0x5C = 92
字节)表示两次快照间新增了92
字节未释放的内存+1
:表示在此期间有1
次内存分配未被释放- 这些数据表明在两次快照期间,存在 内存泄漏
- 明确指向代码
main
函数第52
行,即未释放的new
操作位置
!heap
!heap -s
-
WinDbg
中用于 显示堆的摘要信息 的命令,主要用于快速查看进程内所有堆的全局状态- 堆内存统计:显示每个堆的保留(
Reserved
)和提交(Committed
)内存大小 - 堆块分布分析:展示空闲块(
Free Blocks
)与已分配块(Busy Blocks
)的数量及大小分布 - 堆管理器状态:验证堆元数据完整性(需配合
-v
参数),检测潜在内存损坏
- 堆内存统计:显示每个堆的保留(
-
示例如下:
NtGlobalFlag
:当前启用的全局调试标志stack back traces
:表示堆分配时会记录调用栈(需配合ust
标志),用于追踪内存泄漏或越界操作LFH Key
:低碎片堆(Low Fragmentation Heap
,LFH
)的加密密钥,用于验证堆操作的合法性,防止堆溢出攻击Termination on corruption
:检测到堆内存损坏时自动终止进程,防止进一步破坏系统稳定性Heap
:堆的起始地址(虚拟内存地址)
0000000001cc0000
表示第一个堆的基址Flags
:堆的属性标志(十六进制值),反映堆的配置特性
08000002
表示堆支持动态扩展、启用LFH
Reserv (k)
:堆保留的虚拟内存总量(单位:KB
),即操作系统为此堆预留的地址空间
1800
表示保留6,144 KB
(1.5GB
)Commit (k)
:堆已提交的物理内存量(单位:KB
),即实际被占用的内存
164
表示已提交164 KB
Virt (k)
:堆当前可用的虚拟内存量(单位:KB
)
1020
表示虚拟内存剩余1,020 KB
Free (k)
:堆中空闲内存块的总大小(单位:KB
)
5
表示当前堆有5 KB
空闲内存List length
:空闲链表(Free List
)中的内存块数量
11
表示空闲链表包含11
个内存块UCR
:未提交范围(Uncommitted Ranges
)数量,表示堆扩展时未实际分配物理内存的区域数量
1
表示有1
个未提交区域Virt blocks
:连续内存块的数量,反映堆的碎片化程度
0
表示无连续大块内存Lock cont.
:堆锁的争用次数,高频率表示多线程竞争激烈
0
表示当前无锁争用Fast Heap
:是否启用低碎片堆(Low Fragmentation Heap
)
LFH
表示此堆已启用LFH
优化分配效率
- 实际应用:
- 定位内存泄漏:
若某个堆的Commit
持续增长而Free
减少,可能存在未释放的内存块 - 分析碎片化:
Virt blocks
为0
且List length
较高时,说明堆内存碎片化严重,可能影响性能 LFH
优化验证
若Flags
包含LFH
,表示系统已对此堆启用低碎片分配策略,适合高频小内存分配场景
- 定位内存泄漏:
!heap -h <HeapAddress>
-
用于 显示指定堆的详细信息,包括堆的元数据结构、内存段分布、空闲列表状态等
- 堆元数据分析:展示堆的全局管理结构(如
_HEAP
结构体),验证堆完整性 - 内存段分布:列出堆的虚拟地址范围(起始地址、结束地址)及提交内存状态
- 空闲块管理:显示空闲链表的分配情况,用于分析堆碎片化问题
- 显示堆的分配粒度(
Granularity
)、段(Segment
)分布、空闲列表(FreeList
)等底层属性
- 堆元数据分析:展示堆的全局管理结构(如
-
示例如下:
08000002
:
0x08000000
:启用低碎片堆(LFH
)优化
0x00000002
:支持动态扩展(HEAP_GROWABLE
)Segment at 0000000001cc0000 to 0000000001dbf000
0000000001cc0000
到0000000001dbf000
,总保留空间为00027000 bytes
(159 KB
)
00027000 bytes
中已提交00000162 bytes
(354 bytes
)供实际使用Granularity
(粒度):16 bytes
,堆分配的最小单位Segment Reserve/Commit
:保留00100000
(1 MB
),每次提交00002000
(8 KB
)Total Free Size
:00000162 bytes
(354
字节),空闲内存较少,碎片化程度低FreeList[00]
:空闲链表头位于0000000001cc0150
,包含11 blocks
空闲块
包含11
个空闲块,但总空闲内存仅占堆的0.2%
,说明内存利用率较高
-
示例如下,堆条目解析:
address
:内存块起始地址(如0000000001cc0000
)psize
:前一块的大小(用于内存合并优化)
0
(起始块无前驱)size
:当前块的总大小(单位:字节)flags
:状态标识(如[101]
表示占用块头部信息)state
:busy
(已分配)或free
(空闲)requested size
:用户实际请求的内存大小(可能小于size
因对齐)Internal
:表示该块为堆内部管理结构(如元数据),非用户数据Uncommitted
:未提交的虚拟内存区域(如000d8000
未提交空间)
!heap -p -a <HeapAddress>
-
主要用于 分析启用了页堆(
Page Heap
)的堆块分配信息,具体功能包括:- 堆块元数据解析:显示堆块的分配大小、用户指针地址、填充模式(如
0xABABABAB
)等 - 调用栈回溯:若启用了用户模式堆栈跟踪(
+ust
),可显示分配时的调用栈(需符号文件支持) - 内存破坏检测:通过填充字节验证(如被篡改的填充模式)检测堆溢出或非法访问问题
- 显示分配块的元数据(如
DPH_HEAP_BLOCK
结构)、调用栈跟踪(需启用ust
标志),直接定位分配代码位置
- 堆块元数据解析:显示堆块的分配大小、用户指针地址、填充模式(如
-
示例分析如下:
_HEAP @ 1cc0000
表示该内存块属于地址为1cc0000
的堆HEAP_ENTRY
: 元数据UserPtr
:用户数据起始地址0000000001cc0770
(0000000001cc0740 + 0x30
,因堆头占用0x30
字节)UserSize
:用户请求的分配大小为0x30
字节(48
字节)Flags
:[00]
表示普通堆块(非低碎片堆LFH
或页堆)State
:busy
表示该内存块已被分配且未释放
!heap -stat -h <HeapAddress>
-
用于 统计指定堆的内存分配情况,按块大小分组显示各分组的分配数量及占用的总内存量
- 内存分布分析:按分配大小(如
16
字节、32
字节等)分类统计,快速识别高频分配或潜在泄漏点 - 堆使用趋势判断:通过不同大小块的数量和占比,评估堆的碎片化程度
- 异常分配检测:发现异常大小的内存块(如远大于预期的分配)
- 按分配块大小分组统计,显示
AllocSize
、#Blocks
和TotalMem
,用于识别高频分配的内存大小
- 内存分布分析:按分配大小(如
-
示例分析如下:
size
:内存块大小(十六进制,单位:字节)#blocks
:该大小的内存块数量total
:该大小内存块占用的总字节数(十六进制)(%)
:占堆中“繁忙内存”(已分配内存)的比例
-
示例分析如下:
!heap -flt s <Size>
- 用于按指定大小筛选并分析堆内存块的关键命令
- 显示指定大小(十六进制)的所有分配块地址,适用于排查特定大小的内存泄漏
定位泄露点
!heap -x <LeakAddr>
- 查找包含指定地址的堆
!heap -p -a <LeakAddr>
- 分析泄漏块的调用栈
查看堆段分布
!heap -m <HeapAddress>
- 显示堆的段(
Segment
)地址范围
- 显示堆的段(
检查堆元数据完整性
!heap -validate
- 验证堆结构是否损坏(需启用页堆)
动态修改调试选项
!heap -p -enable 0x1000
- 临时启用堆栈跟踪(仅当前会话有效)
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ WindowsETW进程监控相关03/17
- ♥ Windows进程通信相关03/10
- ♥ Windows 核心编程 _ 内核对象:线程同步三07/31
- ♥ breakpad记述:Windows07/27
- ♥ Windows 核心编程 _ 线程内幕07/06
- ♥ Soui六06/01