练习 2 - 跟踪用户模式进程分配

堆分配直接通过堆 API(HeapAlloc、HeapRealloc 和 C/C++ 分配,如 new、alloc、realloc、calloc)进行,并且使用三种类型的堆提供服务:

  1. 主线 NT 堆 - 提供大小小于 64 KB 的分配请求服务。

  2. 低碎片堆 - 由提供固定大小块的分配请求服务的子段组成。

  3. VirtualAlloc - 提供大小大于 64 KB 的分配请求服务。

VirtualAlloc 用于直接通过“VirtualAlloc”API 进行的大型动态内存分配。 典型用法通常用于位图或缓冲区。 可以使用“VirtualAlloc”保留一个页面块,然后对“VirtualAlloc”进行其他调用,以提交保留块中的各个页面。 这使进程能够保留其虚拟地址空间的范围,而无需使用物理存储,直到需要为止。

在此领域,需要理解以下两个概念:

  1. 保留内存:保留使用的地址范围,但不获取内存资源。

  2. 已提交内存:确保在引用地址时物理内存或页面文件空间可用。

在此练习中,你将了解如何收集跟踪来调查用户模式进程分配内存的方式。

本练习重点介绍名为 MemoryTestApp.exe 的虚拟测试进程,该进程通过以下方法分配内存:

  1. “VirtualAlloc”API,用于提交大型内存缓冲区。

  2. The C++ new 运算符,用于实例化小型对象。

可从此处下载 MemoryTestApp.exe。

步骤 1:使用 WPR 收集 virtualAlloc/堆跟踪

大型内存分配通常是影响进程占用空间的分配,由“VirtualAlloc”API 提供服务。 所有调查都应该从此处着手,但也有可能是进程在分配较小的情况下误操作(例如,在 C++ 中使用 new 运算符的内存泄漏等)。 发生此情况时,堆跟踪变得非常有用。

步骤 1.1:为堆跟踪准备系统

当“VirtualAlloc”分析未提供有关内存使用问题的任何相关解释时,堆跟踪应被视为可选且已完成。 堆跟踪往往会生成更大的跟踪,建议仅为正在调查的各个进程启用跟踪。

为关注的进程添加注册表项(此例中为 MemoryTestApp.exe);然后,将针对每个后续进程创建启用堆跟踪。

reg add "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\MemoryTestApp.exe" /v TracingFlags /t REG_DWORD /d 1 /f

步骤 1.2:使用 WPR 捕获跟踪

在此步骤中,将使用包含“VirtualAlloc”和“堆”数据的 WPR 收集跟踪。

  1. 打开“WPR”并修改跟踪配置。

    1. 选择“VirtualAlloc”和“堆”提供程序。

    2. 选择“常规”作为“性能方案”。

    3. 选择“常规”作为“日志记录模式”。

      WPR 跟踪选项菜单的屏幕截图。

  2. 单击“启动”以启动跟踪。

  3. 启动 MemoryTestApp.exe,并等待进程终止(大约需要 30 秒)。

  4. 返回到“WPR”,保存跟踪,然后使用“Windows 性能分析器 (WPA)”将其打开。

  5. 打开“跟踪”菜单,然后选择“配置符号路径”。

    • 指定符号缓存的路径。 有关符号详细信息,请参阅 MSDN 上的符号支持页面。
  6. 打开“跟踪”菜单,然后选择“负载符号”。

现在,你有一个跟踪,其中包含 MemoryTestApp.exe 进程在其生命周期内的所有内存分配模式。

步骤 2:查看 VirtualAlloc 动态分配

详细的“VirtualAlloc”数据通过 WPA 中的“VirtualAlloc 提交生存周期”图公开。 关注的重要列如下所示:

描述
Process

通过“VirtualAlloc”执行内存分配的进程名称。

提交堆栈

显示导致分配内存的代码路径的调用堆栈。

提交时间

分配内存的时间戳。

取消提交时间

释放内存的时间戳。

影响大小

未完成分配的大小或所选时间间隔的开始和结束之间的大小差异。 此大小根据所选视图端口进行调整。

如果一个进程分配的所有内存在 WPA 中的可视化间隔结束时被释放,则“影响大小”值将为零。

大小

所选时间间隔内所有分配的累计总和。

按照以下步骤分析 MemoryTestApp.exe

  1. 在“Graph 浏览器”的“内存”类别中找到“VirtualAlloc 提交生命周期”图。

  2. 将“VirtualAlloc 提交生命周期”图拖放到“分析”选项卡上。

  3. 组织表以显示这些列。 右键单击列标题以添加或删除列。

    1. 处理

    2. 影响类型

    3. 提交堆栈

    4. 提交时间和取消提交时间

    5. 计数

    6. 影响大小和大小

  4. 在进程列表中找到 MemoryTestApp.exe。

  5. 应用筛选器以仅在图上保留 MemoryTestApp.exe。

    • 右键单击并选择“筛选”来选定内容。

显示如何筛选结果的屏幕截图。

分析视区应类似于以下内容:

数据在筛选后的外观示例图。

在上一示例中,有两个值值得关注:

  • 大小为 126 MB:这表明 MemoryTestApp.exe 在其生命周期内总共分配了 125 MB。 它表示进程及其依赖项进行的所有“VirtualAlloc”API 调用的累积总和。

  • 影响大小为 0 MB:这表示当前分析的时间间隔结束时将释放进程分配的所有内存。 系统未受到其稳定状态内存使用增加的影响。

步骤 2.1:分析稳定状态内存使用

调查内存分配时,应尝试回答以下问题:“为什么在这种情况下稳定状态内存使用会增加?” 在 MemoryTestApp.exe 示例中,可以看到它在开始时分配到约 10 MB 的稳定状态内存,然后在中途增加至 20 MB。

显示内存使用情况的示例数据的屏幕截图。

若要调查此行为,请将缩放范围缩小到跟踪中途突然增加时的时间间隔。

显示如何放大数据的屏幕截图。

该视区应如下所示。

使用“VirtualAlloc 提交生命周期”和“按进程显示未完成提交”应用缩放选项显示图后的示例数据的屏幕截图

如你所见,“影响大小”现在为 10 MB。 这意味着,在分析的时间间隔的开始和结束之间,稳定状态内存使用量会增加 10 MB。

  1. 通过单击列标题,按“影响大小”排序。

  2. 展开“MemoryTestApp.exe”行(在“进程”列中)。

  3. 展开“影响”行(在“影响类型”列中)。

  4. 在“提交堆栈”进程中导航,直到找到分配了 10 MB 内存的函数。

    示例数据表的屏幕截图,其中显示了行号、进程、影响类型、提交堆栈、提交时间、取消提交时间、计数和影响大小

此示例中,MemoryTestApp.exe 的“主要”函数通过直接调用“VirtualAlloc”在工作负载中分配 10 MB 内存。 在现实世界中,应用程序开发人员应确定分配是否合理,或者是否可重新排列代码以最大程度地减少稳定状态内存使用量的增加。

你现在可以在 WPA 中“取消缩放”视区。

取消缩放菜单的屏幕截图。

步骤 2.2:分析暂时性(或峰值)内存使用情况

调查内存分配时,应尝试回答问题:“为什么在这种情况下内存使用会出现短暂性峰值?” 暂时性分配会导致内存使用激增,并可能在出现内存压力时导致碎片并将有价值的内容推送到系统待机缓存外。

在 MemoryTest 示例中,可以看到,有 10 个不同的内存使用峰值 (10 MB) 均匀分布在跟踪中。

显示内存使用情况数据的图表的屏幕截图。

将缩放范围缩小到最后四个峰值,以将关注点放在一个较小的区域上,并减少非相关行为的干扰。

缩放选项的屏幕截图。

该视区应如下所示:

显示使用缩放选项的内存使用情况数据的图表的屏幕截图。

  1. 通过单击列标题,按“大小”排序。

  2. 展开“MemoryTestApp.exe”行(在“进程”列中)。

  3. 单击“暂时性”行(在“影响类型”列中)。

    • 这应该以蓝色突出显示视区中内存使用的所有峰值。
  4. 请注意不同列的值:

    1. 计数 = 4:这表示在该时间间隔内进行了四次暂时性内存分配。

    2. 影响大小 = 0 MB:这表示在该时间间隔结束时释放了所有四个暂时性内存分配。

    3. 大小 = 40 MB:这表示所有四个暂时性内存分配的总和为 40 MB 内存。

  5. 在“提交堆栈”进程中导航,直到找到分配了 40 MB 内存的函数。

    内存使用情况数据的屏幕截图。

在此示例中,MemoryTestApp.exe 的“Main”函数调用名为“Operation1”的函数,该函数反过来调用名为“ManipulateTemporaryBuffer”的函数。 然后,此“ManipulateTemporaryBuffer”函数直接调用“VirtualAlloc”四次,每次创建和释放 10 MB 内存缓冲区。 每个缓冲区仅持续 100 毫秒。 缓冲区的分配时间和释放时间由“提交时间”和“取消提交时间”列表示。

在现实世界中,应用程序开发人员确定是否需要这些短期的暂时性临时缓冲分配,或者是否可以使用永久内存缓冲区执行操作以将其替换。

现在可以在“WPA”中“取消缩放”视区。

步骤 3:查看堆动态分配

到目前为止,分析仅侧重于由“VirtualAlloc”API 提供服务的大型内存分配。 下一步是确定使用最初收集的堆数据时,该进程所做的其他小型分配是否存在问题。

详细堆数据通过 WPA 中的“堆分配”图公开。 关注的重要列如下所示:

描述
Process 正在执行内存分配的进程的名称。
Handle

用于为分配提供服务的堆的标识符。

可以创建堆,因此该进程可能有多个堆句柄。

堆叠 显示导致分配内存的代码路径的调用堆栈。
分配时间 分配内存的时间戳。
影响大小 未完成分配的大小或所选视区的开始和结束之间的大小差异。 此大小根据所选时间间隔进行调整。
大小 所有分配/取消分配的累计和。

按照以下步骤分析 MemoryTestApp.exe

  1. 在“Graph 浏览器”的“内存”类别中找到“堆分配”图。

  2. 将“堆分配”图拖放到“分析”选项卡上。

  3. 组织表以显示这些列:

    1. 处理

    2. Handle

    3. 影响类型

    4. 堆叠

    5. 分配时间

    6. 计数

    7. 影响大小和大小

  4. 在进程列表中找到 MemoryTestApp.exe。

  5. 应用筛选器以仅在图上保留 MemoryTestApp.exe。

    • 右键单击并选择“筛选”来筛选选定内容。

该视区应如下所示:

示例数据的屏幕截图,其中显示了“按进程和句柄显示未完成大小的堆分配”图

在此示例中,可以看到其中一个堆的大小随着时间的推移以恒定的速度稳步增加。 该堆上有 1200 个内存分配,在间隔结束时占用的已用内存达 130 KB。

  1. 放大显示跟踪时的较小间隔(例如 10 秒)。

  2. 展开显示最大分配量的句柄头(如“影响大小”列中所示)。

  3. 展开“影响”类型。

  4. 在进程“堆栈”中导航,直至找到负责分配所有此内存的函数。

    示例数据表的屏幕截图,其中显示了进程、句柄、影响类型、堆栈、分配时间、计数、影响大小和大小,有两行已被选中

在此示例中,MemoryTestApp.exe 的“Main”函数调用名为“InnerLoopOperation”的函数。 然后,此“InnerLoopOperation”函数通过 C++ new 运算符分配 40 字节的内存 319 次。 此内存将保持分配状态,直至进程终止。

在现实世界中,应用程序开发人员则应该确定此行为是否表示可能存在内存泄漏,并解决问题。

步骤 4:清理测试系统

分析完成后,应清理注册表,以确保对该进程禁用了堆跟踪。 在权限提升的命令提示符中运行此命令:

reg delete "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\MemoryTestApp.exe" /v TracingFlags /f