• 忘掉天地
  • 仿佛也想不起自己
  • 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-03-28 23:37 Aet 隐藏边栏 |   抢沙发  4 
文章评分 1 次,平均分 5.0

调试堆内存

启用用户堆栈跟踪(User Stack Trace

  1. 方法一
    1. 通过 gflags.exe 为目标进程添加堆栈跟踪标志:

  1. 方法二:
    1. 打开Global Flags,设置如图示:

相关问题

  1. 在实践过程中,发现勾选Enable page heap之后,虽然点击上图的应用确认之后,查看注册表,值为0x02001070,同时用gflags.execmd里面打印的也是0x02001070,看似正确:
    1. 但实际上,在windbg里面,用!gflag打印出来的值为0x02000000
    2. 经实践,如果不勾选Enable page heap只勾选上图左边四项,注册表值为0x00001070,且用!gflag打印出的值也是0x00001070

验证UST是否生效

  1. 方法一
    1. Win + R,输入regedit打开注册表
    2. 定位到 Image File Execution Options 路径:
      aet_breakpad_test.exe的键不存在,说明没设置成功
    3. 检查GlobalFlag的值
      UST 对应的标志值为 0x1000(即启用用户堆栈跟踪)

  1. 方法二
    1. 启动aet_breakpad_test.exe
    2. 打开windbg附加到进程(或者直接用windbg启动不需要附加)
    3. !gflag:查看全局标志
    4. heap -p -a [堆块地址]:检查堆分配调用栈
      若分配堆块时能显示调用栈(如 Alloc 函数的堆栈回溯),则 UST 生效

!gflag

概述

GlobalFlag

  1. 0x02000000
    1. 启用 hpaHeap Placement at Ends of Pages
  2. 0x00001000
    1. 启用 ustUser Stack Trace
  3. 0x00000070
    1. 启用 Heap Tail Checking

hpa

  1. 概述
    1. 页堆(Page Heap) 是 Windows 堆管理器提供的调试功能,用于检测堆内存溢出、越界访问等问题
    2. 其核心原理是通过在堆块后附加 不可访问的栅栏页(Guard Page),当程序试图越界写入或读取时,会立即触发访问违规异常(Access Violation),从而快速定位问题
  2. 特性:
    1. 内存隔离:每个堆块独占一块内存区域,避免相邻堆块干扰
    2. 溢出检测:堆块尾部设置不可访问的栅栏页,溢出操作直接触发崩溃
    3. 精准定位:异常发生时,调试器可直接捕获到导致溢出的代码位置

ust

  1. 概述
    1. 用户态栈回溯(User Stack Trace, UST)是一种在程序运行中记录函数调用链的技术,用于快速定位内存泄漏、越界访问等问题
    2. 通过 gflag 工具启用 UST 后,堆管理器会在内存分配时记录调用栈信息,便于后续调试器(如 WinDbg)通过 !heap -p -a 命令查看完整的函数调用路径
  2. 场景
    1. 内存泄漏排查:追踪未释放堆块的分配源头
    2. 越界访问调试:结合页堆(Page Heap)捕获越界操作的调用栈
    3. 多线程问题分析:识别线程竞争导致的资源未同步释放

htc

  1. 概述
    1. 堆尾检查(Heap Tail Checking, HTC) 是 Windows 堆管理器提供的一项调试功能,用于检测堆内存的尾部溢出问题
  2. 其核心原理是:
    1. 在堆块的尾部添加特定填充字节(通常为 0xABABABAB),当程序尝试越界写入堆块尾部时,这些填充字节会被破坏,堆管理器在释放内存或主动检查时触发异常,帮助开发者快速定位溢出位置
  3. 场景
    1. 越界写入检测:例如数组索引超出范围或字符串拷贝未截断
    2. 内存破坏分析:如全局变量覆盖堆块尾部数据
    3. 多线程同步问题:并发操作导致的堆块边界冲突

堆内存

操作系统级内存分配流程

  1. 当进程启动时,操作系统为其分配虚拟地址空间(Virtual Address Space),但此时仅建立虚拟页表,物理内存页并未实际分配
  2. 首次访问某个虚拟页时触发 页错误(Page Fault),操作系统才会通过 VirtualAllocAPI 提交物理内存页,并更新页表映射

new的调用链路

  1. C++new 操作符通过以下链路与操作系统交互:
    1. new → operator new → malloc → HeapAlloc → VirtualAlloc(最终路径)
  2. operator new 的默认实现
    1. 默认情况下,operator new 调用 malloc(如 Microsoft CRT 的实现),而 malloc 在 Windows 中底层调用 HeapAlloc
  3. HeapAllocVirtualAlloc 的协作
    1. HeapAlloc 管理进程的默认堆(Default Heap),当堆内存不足时,会调用 VirtualAlloc 向操作系统申请更大的内存块(通常以 1MB 为单位),然后分割为小块供 HeapAlloc 管理

new 与物理内存提交的延迟性

  1. 初次访问触发提交
    1. 即使 new 成功返回指针,对应的物理内存页可能尚未分配。例如:
    2. 此时,p[0] 的写入操作会触发页错误,操作系统通过 VirtualAlloc 提交物理内存

性能优化策略

  1. 预提交内存(MEM_COMMIT
    1. 通过 VirtualAllocMEM_COMMIT 标志强制提交物理内存,避免首次访问的延迟:
    2. 此方法常用于实时性要求高的场景(如高频交易系统)

总结(记忆)

  1. new在堆上分配的内存,本质上是在进程的虚拟地址空间里面的堆区,去访问虚拟页,虽然可能成功获得了指针,但是对应的物理内存的页框可能并未分配
    1. new 在堆上分配内存时,C++ 运行时库(如 malloc)会向操作系统的 堆管理器(如 WindowsHeapAllocLinuxbrk/mmap)请求虚拟地址空间
    2. 操作系统仅在进程首次访问虚拟页时,才会通过 缺页中断(Page Fault) 分配物理页框

UMDHUser-Mode Dump Heap

概述

  1. Windows 平台下用于检测用户态程序堆内存泄漏的专业工具,属于 Windbg 工具集的一部分
  2. 它通过记录程序运行过程中堆内存分配的调用栈信息,对比不同时间点的内存快照差异,定位未释放的内存块及其源头代码

工作原理

  1. 堆内存跟踪机制
    1. UMDH 依赖于 Windows 堆管理器的调试功能
    2. 通过启用 gflags 工具的 用户态堆栈追踪数据库(User-Mode Stack Trace Database),UMDH 可捕获每次内存分配(malloc/new)时的调用栈信息
  2. 快照对比分析
    1. 通过两次内存快照(begin.logend.log)的差异,UMDH 识别出在两次快照间新增且未释放的内存分配记录,并关联到具体的代码位置
    2. 例如,若某段代码在两次快照间分配了 100 次内存但未释放,UMDH 会标记该调用栈为潜在泄漏点
  3. 符号解析支持
    1. 需正确配置符号路径(_NT_SYMBOL_PATH),确保 UMDH 能解析程序及系统库的调试符号(PDB 文件),将内存地址映射到源码行号

UMDH 使用步骤

  1. 启用堆栈追踪
    1. 使用 gflags 为目标程序开启堆栈追踪:
    2. 详细看上面的启用UST
  2. 配置符号路径
    1. 设置符号路径,包含程序 PDB 文件和微软符号服务器:

  1. 生成内存快照

  1. 对比快照差异
    1. 生成差异报告:
    2. 报告中将列出新增内存分配的调用栈及对应的源码位置

关于工具使用

  1. 需通过命令提示符(CMD)或PowerShell执行相关命令

begin.log

end.log

diff.log

  1. 部分内容如下
    1. +5c:(0x5C = 92字节)表示两次快照间新增了 92字节未释放的内存
    2. +1:表示在此期间有 1次内存分配未被释放
    3. 这些数据表明在两次快照期间,存在 内存泄漏
    4. 明确指向代码 main 函数第 52行,即未释放的 new 操作位置

!heap

!heap -s

  1. WinDbg 中用于 显示堆的摘要信息 的命令,主要用于快速查看进程内所有堆的全局状态

    1. 堆内存统计:显示每个堆的保留(Reserved)和提交(Committed)内存大小
    2. 堆块分布分析:展示空闲块(Free Blocks)与已分配块(Busy Blocks)的数量及大小分布
    3. 堆管理器状态:验证堆元数据完整性(需配合 -v 参数),检测潜在内存损坏
  2. 示例如下:

    1. NtGlobalFlag:当前启用的全局调试标志
    2. stack back traces:表示堆分配时会记录调用栈(需配合 ust 标志),用于追踪内存泄漏或越界操作
    3. LFH Key:低碎片堆(Low Fragmentation Heap, LFH)的加密密钥,用于验证堆操作的合法性,防止堆溢出攻击
    4. Termination on corruption:检测到堆内存损坏时自动终止进程,防止进一步破坏系统稳定性
    5. Heap:堆的起始地址(虚拟内存地址)
      0000000001cc0000 表示第一个堆的基址
    6. Flags:堆的属性标志(十六进制值),反映堆的配置特性
      08000002 表示堆支持动态扩展、启用 LFH
    7. Reserv (k):堆保留的虚拟内存总量(单位:KB),即操作系统为此堆预留的地址空间
      1800 表示保留 6,144 KB1.5GB
    8. Commit (k):堆已提交的物理内存量(单位:KB),即实际被占用的内存
      164 表示已提交 164 KB
    9. Virt (k):堆当前可用的虚拟内存量(单位:KB
      1020 表示虚拟内存剩余 1,020 KB
    10. Free (k):堆中空闲内存块的总大小(单位:KB
      5 表示当前堆有 5 KB 空闲内存
    11. List length:空闲链表(Free List)中的内存块数量
      11 表示空闲链表包含 11 个内存块
    12. UCR:未提交范围(Uncommitted Ranges)数量,表示堆扩展时未实际分配物理内存的区域数量
      1 表示有 1 个未提交区域
    13. Virt blocks:连续内存块的数量,反映堆的碎片化程度
      0 表示无连续大块内存
    14. Lock cont.:堆锁的争用次数,高频率表示多线程竞争激烈
      0 表示当前无锁争用
    15. Fast Heap:是否启用低碎片堆(Low Fragmentation Heap
      LFH 表示此堆已启用 LFH 优化分配效率

  1. 实际应用:
    1. 定位内存泄漏:
      若某个堆的 Commit 持续增长而 Free 减少,可能存在未释放的内存块
    2. 分析碎片化:
      Virt blocks0List length 较高时,说明堆内存碎片化严重,可能影响性能
    3. LFH 优化验证
      Flags 包含 LFH,表示系统已对此堆启用低碎片分配策略,适合高频小内存分配场景

!heap -h <HeapAddress>

  1. 用于 显示指定堆的详细信息,包括堆的元数据结构、内存段分布、空闲列表状态等

    1. 堆元数据分析:展示堆的全局管理结构(如 _HEAP 结构体),验证堆完整性
    2. 内存段分布:列出堆的虚拟地址范围(起始地址、结束地址)及提交内存状态
    3. 空闲块管理:显示空闲链表的分配情况,用于分析堆碎片化问题
    4. 显示堆的分配粒度(Granularity)、段(Segment)分布、空闲列表(FreeList)等底层属性
  2. 示例如下:

    1. 08000002
      0x08000000:启用低碎片堆(LFH)优化
      0x00000002:支持动态扩展(HEAP_GROWABLE
    2. Segment at 0000000001cc0000 to 0000000001dbf000
      0000000001cc00000000000001dbf000,总保留空间为 00027000 bytes159 KB
      00027000 bytes 中已提交 00000162 bytes354 bytes)供实际使用
    3. Granularity(粒度):16 bytes,堆分配的最小单位
    4. Segment Reserve/Commit:保留 001000001 MB),每次提交 000020008 KB
    5. Total Free Size00000162 bytes354 字节),空闲内存较少,碎片化程度低
    6. FreeList[00]:空闲链表头位于 0000000001cc0150,包含 11 blocks 空闲块
      包含 11 个空闲块,但总空闲内存仅占堆的 0.2%,说明内存利用率较高
  3. 示例如下,堆条目解析:

    1. address:内存块起始地址(如 0000000001cc0000
    2. psize:前一块的大小(用于内存合并优化)
      0(起始块无前驱)
    3. size:当前块的总大小(单位:字节)
    4. flags:状态标识(如 [101] 表示占用块头部信息)
    5. statebusy(已分配)或 free(空闲)
    6. requested size:用户实际请求的内存大小(可能小于 size 因对齐)
    7. Internal:表示该块为堆内部管理结构(如元数据),非用户数据
    8. Uncommitted:未提交的虚拟内存区域(如 000d8000 未提交空间)

!heap -p -a <HeapAddress>

  1. 主要用于 分析启用了页堆(Page Heap)的堆块分配信息,具体功能包括:

    1. 堆块元数据解析:显示堆块的分配大小、用户指针地址、填充模式(如 0xABABABAB)等
    2. 调用栈回溯:若启用了用户模式堆栈跟踪(+ust),可显示分配时的调用栈(需符号文件支持)
    3. 内存破坏检测:通过填充字节验证(如被篡改的填充模式)检测堆溢出或非法访问问题
    4. 显示分配块的元数据(如 DPH_HEAP_BLOCK 结构)、调用栈跟踪(需启用 ust 标志),直接定位分配代码位置
  2. 示例分析如下:

    1. _HEAP @ 1cc0000 表示该内存块属于地址为 1cc0000 的堆
    2. HEAP_ENTRY: 元数据
    3. UserPtr:用户数据起始地址 0000000001cc07700000000001cc0740 + 0x30,因堆头占用 0x30 字节)
    4. UserSize:用户请求的分配大小为 0x30 字节(48 字节)
    5. Flags[00] 表示普通堆块(非低碎片堆 LFH 或页堆)
    6. Statebusy 表示该内存块已被分配且未释放

!heap -stat -h <HeapAddress>

  1. 用于 统计指定堆的内存分配情况,按块大小分组显示各分组的分配数量及占用的总内存量

    1. 内存分布分析:按分配大小(如 16 字节、32 字节等)分类统计,快速识别高频分配或潜在泄漏点
    2. 堆使用趋势判断:通过不同大小块的数量和占比,评估堆的碎片化程度
    3. 异常分配检测:发现异常大小的内存块(如远大于预期的分配)
    4. 按分配块大小分组统计,显示 AllocSize#BlocksTotalMem,用于识别高频分配的内存大小
  2. 示例分析如下:

    1. size:内存块大小(十六进制,单位:字节)
    2. #blocks:该大小的内存块数量
    3. total:该大小内存块占用的总字节数(十六进制)
    4. (%):占堆中“繁忙内存”(已分配内存)的比例
  3. 示例分析如下:

!heap -flt s <Size>

  1. 用于按指定大小筛选并分析堆内存块的关键命令
  2. 显示指定大小(十六进制)的所有分配块地址,适用于排查特定大小的内存泄漏

定位泄露点

  1. !heap -x <LeakAddr>
    1. 查找包含指定地址的堆

  1. !heap -p -a <LeakAddr>
    1. 分析泄漏块的调用栈

查看堆段分布

  1. !heap -m <HeapAddress>
    1. 显示堆的段(Segment)地址范围

检查堆元数据完整性

  1. !heap -validate
    1. 验证堆结构是否损坏(需启用页堆)

动态修改调试选项

  1. !heap -p -enable 0x1000
    1. 临时启用堆栈跟踪(仅当前会话有效)

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2025-03-29
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享