【问题标题】:What does "Private Data" define in VMMAP?VMMAP 中的“私有数据”定义了什么?
【发布时间】:2016-01-06 08:43:30
【问题描述】:

我正在使用 VMMap 分析混合模式(托管和非托管)应用程序中的虚拟/进程地址空间利用率。我了解 Windows VMM 和虚拟内存 API 的工作原理,我也了解堆内存 API 的工作原理。我已经查看了我正在使用的 CRT 实现(不是很详细)并且(我想我 - 这可能是我的失败)了解它如何使用上述 Win32 API。

我希望了解这个“私人数据”统计数据向我展示了什么。我的应用程序没有直接调用任何 Win32 内存 API 函数,它只在本机 C++ 中使用“malloc/new”,在 C# 中使用“new”(这将深入使用 Win32 内存管理 API)。

VMMap给出的“私有数据”的定义是:

私有内存是由 VirtualAlloc 分配的内存,而不是 由堆管理器或 .NET 运行时重新分配。它 不能与其他进程共享,对系统收费 提交限制,通常包含应用程序数据。

所以我想这个定义让我问,好吧,那么是谁在调用 VirtualAlloc?是堆管理器还是 .Net 运行时?

我可以获取一些已提交的私有数据的地址并使用 WinDbg 来找出......嗯......事实证明微软在他们的智慧中已经对 ntdll 公共符号进行了贵族化,所以 WinDbg 不能正常工作很好 - 如果需要,我可以提供更多详细信息,但基本上像 !address -summary 这样的命令由于缺少符号而不再起作用。

这个问题的另一种表达方式可能是:我可以编写哪些 C++ 或 C# 代码来导致该私有数据统计量增加或减少?还是这一切都由操作系统、C++ 运行时或 .Net 运行时管理,因此任由其一时兴起?

我可以从 VMMap 的性质(其他内存类型是互斥的)推断出这种“私有数据”,因此不能是以下任何类型的地址空间:

  • 堆(请注意,这包括已提交和保留的堆空间 - 保留 通过调用 VirtualAlloc,如描述中所述 上面的私人数据)。
  • 托管堆
  • 堆栈
  • 可分享
  • 映射文件
  • 图片
  • 页表
  • 无法使用
  • 免费

(我找不到定义VMMap认为上述所有类型的在线帮助文​​件,但这里有一个下载帮助文件的链接:https://technet.microsoft.com/en-us/library/dd535533.aspx

我注意到,在我的应用程序中,私有数据的 TOTAL(保留和提交)大小在我的应用程序生命周期内保持相当稳定,尽管堆/托管堆/堆栈大小按预期变化。我还注意到,在私有数据使用的约 250Mb 总量中,实际上只有约 33Mb 被提交。请注意,我的测量方法相当初级,因此值可能在我的每次测量之间发生变化,而我只是没有看到它(如果我知道这是在测量什么,我可以使用 DebugDiag 来获取进程的转储相关计数器达到一定的阈值,鸡和蛋)。

我目前的推测理论是,这是保留的空间,用于在本地(或我想是管理的?)堆达到其容量时增长,但我没有任何证据可以证明这一点。所以它仍然牢牢地留在投机堆中。

在互联网上搜索这方面的详细信息可能会很痛苦,那里有很多令人困惑的帖子/文章/博客,使用自引用定义(Performance Monitor 对工作集的定义的第一句话就是一个很好的例子),不完整或完全错误。许多地方模糊了定义或使用了不一致的术语(请注意,VMMaps 字段私有数据的定义继续将其称为私有内存,这可能有点令人反感,但含糊不清)。

现在我已经批评互联网的其他部分让事情变得混乱和不正确......如果上面有任何不合理的地方,或者你可以向我展示相反的文档,或者你需要更多明确的定义,让我知道,我也会把自己列入罪犯名单!我认为尝试在网上向某人解释记忆问题的前半部分是确保我们都在谈论同一件事。

最后这个问题:How does VMMap know a given memory region is Thread Stack, specifically? 表明我可能永远找不到答案:/

更新/编辑:我发现通过打开 gflags 用户堆栈跟踪(gflags -i myapp.exe +ust),您可以增加私有数据的大小,我假设这是回溯数据库,但是有即使没有 gflags,我仍然在努力解释私有数据。

【问题讨论】:

  • 我目前在我的应用程序中遇到了一些非常相似的问题,实际上是在特定环境中导致 OutOfMemoryException。我设法手动创建了一些 C# 代码来 PInvoke VirtualAlloc(来自 kernel32.dll),它生成了相同的 VMMap 结果,分配了大量的“私有数据”(参见this link)。尽管如此,我仍然试图跟踪这个“私人数据”是从哪里分配的。我正在尝试使用 VMMap 来跟踪该数据,但到目前为止没有成功。

标签: c# c++ winapi memory vmmap


【解决方案1】:

我知道这是一个很老的问题。但由于某种原因,它仍然没有得到答复。 Sasha Goldstein 在 DotNext 会议上谈论 WinDbg 时提到了这个问题 - https://www.youtube.com/watch?v=8t1aTbnZ2CE。 关键是它可以在 WinDbg 的帮助下轻松回答。

为了回答 CLR 是否使用 VirtualAlloc 作为其堆,我们将在此函数处设置一个断点,并使用一个打印当前堆栈(本机和托管)的脚本。

bp kernelbase!VirtualAlloc ".printf \"allocating %d bytes of virtual memory\", dwo(@esp+8);.echo;  k 5; !clrstack; gc"

这里:k 5 打印本机调用堆栈的最后 5 帧,!clrstack(来自 SOS)打印托管堆栈。 gc 继续执行。

请注意,此脚本仅适用于 x86 进程。对于 x64,您将需要其他一些(注册表和调用约定不同)。

然后我创建了一个简单的程序,它分配一个对象并将其添加到一个列表中。

    static void Main(string[] args)
    {
        var list = new List<string[]>();
        while (true) {
            var a = Console.ReadLine();
            if (a == "q" || a == "Q") break;
            var arr = new string[100];
            list.Add(arr);
        }
    }

在 WinDbg 下运行它并开始按 Enter。在某些时候,断点命中 - 列表扩展并在堆中分配额外的内存:

显然,CLR 使用 VirtualAlloc 为其堆分配内存。

【讨论】:

  • 感谢您的回复。当我现在看它时,一种明显的解决方案!我不会将其标记为答案,因为它只是提供更多支持信息,但实际上并没有解决“VMMAP 中的“私有数据”定义什么?”
【解决方案2】:

进程的折叠 VMMap 视图显示所有 VAD 条目。可以使用kd / windbg / livekd 查看 VAD。

我们来看看calc.exe

lkd> !process 0n12876
PROCESS fffffa802e058600
    SessionId: 1  Cid: 324c    Peb: 7fffffdf000  ParentCid: 0bec
    DirBase: 22211000  ObjectTable: fffff8a00e9b1310  HandleCount:  85.
    Image: calc.exe
    VadRoot fffffa8039b76500 Vads 176 Clone 0 Private 1852. Modified 1. Locked 0.
.
.

lkd> !vad fffffa8039b76500
VAD             level      start      end    commit
.
.
fffffa803c9da680 ( 6)      ff7b0    ff892         6 Mapped  Exe  EXECUTE_WRITECOPY  \Windows\System32\calc.exe
.
.

VAD 只有一个范围,并显示出虚假保护,EXECUTE_WRITECOPY,尽管我认为这确实概括地描述了所有部分,因此其中的部分被允许为 CoW 或只读和可执行。 VMMap 试图提供更多信息,不仅显示图像子部分对象,还显示这些子部分内的不同保护范围。例如,它显示了 1 个.data 子部分,其中的所有保护范围。原来是写页上的 5 个副本,现在 3 个已经被读/写 PTE 替换,2 个仍然保持原样。

.rdata中有一个私有页面,同时.data的读/写和CoW页面都是private.rdata 的私有部分将是包含导入地址表(IAT)的页面,因为它是由加载程序修改的,并且对于每个进程都不同。因此,这也是在写入页面上制作的副本,然后被写入,现在它已被加载器只读。它不需要是可执行的,因为它是使用 rip-relative 内存间接jump or call 访问的。

所有其他部分都有文件支持,并在进程之间共享。可共享工作集是可以被其他进程映射入的工作集,共享工作集是至少被其他进程实际映射入的工作集,是可共享WS的子集。

Private 表示数据是由进程修改的,它只与该进程相关,与其他进程无关,因此不会写回共享映射文件以供其他进程查看。因此,它必须创建一个页面,该页面将存储在页面文件中,而不是在需要分页时写回文件。这方面的一个例子是 IAT,其中每个图像将具有不同的导入地址,具体取决于加载的模块在进程地址空间中的位置,这因进程而异,并且在另一个进程的上下文中没有任何意义。

private 的另一个示例可能是在图像未在其首选基址加载且代码中有绝对地址的情况下需要修复的代码部分的一部分。这些页面必须在写入时分配副本,然后执行/读取 - 在此示例中没有。

还有一点需要注意,size 表示已被进程保留的虚拟内存。例如HeapCreate为堆保留了一些内存,它将有一个VAD条目,并将VAD条目标记的保留的完整大小添加到size(并且VAD中的块大小通过其他计算意味着,比如分析 PTE 本身)。然后当您调用 HeapAlloc 时提交此内存,即实际分配 PTE,这意味着 PDE 也分配了一个物理页面,以便 PTE 可以实际更改。对于特定范围,PTE 的需求为零。这些现在是“承诺的”。当您实际写入地址时,PTE 将被分配一个归零的物理页面,并且 PTE 将成为指向该物理页面的有效硬件 PTE,现在该页面是进程工作集的一部分,直到它从工作集。

private 是私有的虚拟提交(以页面为单位,即 4KiB 粒度),private WS 然后显示由 PTE 表示的私有虚拟提交页面的数量,这些 PTE 实际为它们分配了物理页面作为一部分的过程工作集。它是工作集中物理页面的逻辑分类。 total WSprivate WS + shareable WS,即它是进程的工作集。

映射图像的大小总是与其提交的大小相同,但映射的文件和部分并不总是——它们可以像堆一样保留块。在映射文件的上下文中保留意味着该区域还没有 PTE。您通常映射整个图像部分的视图,但是对于数据文件,我看到了映射完整文件的情况,但 VAD 条目的保留空间比文件大得多——我不确定如何去做这个。映射的大小在 VAD 中保留,然后为它们分配包含 PTE 的物理页面,以便可以同时填充它们的 PTE 以指向正确的原型 PTE (PPTE),这些原型 PTE (PPTE) 已创建(但未分配物理页)when the section was created in the case of image files, and when the view was mapped in the case of data files。当文件实际被访问时,给PTE一个物理页来指向,它成为一个有效的硬件PTE。这将只是从 PPTE 复制指向 PPTE 指向的物理页面的指针(如果 PPTE 没有指向一个,即它是一个MMPTE_SUBSECTION,那么它会被分配并填充一个对文件的 IO 读取, 或者如果原型 PTE 是页面文件 PTE,则它被分页)。

预留只是 VAD 中的预留,但提交意味着现在有软件形式的 PTE(即它们是无效的),这意味着有一些额外的提交费用,因为需要在其中分配物理页面PDE / PDPT / PML4 即 PTE 页,以便 PTE 可以实际写入。这个特定的提交费用不会显示在进程的工作集中(工作集提交费用),分页/非分页池提交费用或修改/备用列表提交费用或页面文件提交费用也不显示。

默认情况下,只读部分是共享部分(除了需要由链接器修改的页面,因为它们暂时变得可写),但图像中启用写入的部分默认情况下不共享,因此它是copy on write,每个进程都有不同的数据段副本,在缺页时替换掉(缺页是因为CoW页无效),需要的时候分页到页文件分页。如果您在图像的节标题特性中将其指定为共享节,则它不会在写入页面上分配为副本,只是读/写,并且所有对它的写入都将直接写入映射的图像,并且会被所有已映射它的进程。我相信这最终会写回图像,即它是文件支持的。

CoW 页面虚拟提交被列为私有的事实很有趣。我认为这个页面本身是共享的和文件支持的,只有替换页面是私有的。在这种情况下,CoW 页面也不在工作集上,但在我的chrome.exe 中,它是全部且仅在可共享的工作集上,如您所料,尽管虚拟提交仍然列出作为私有的,但至少它在可共享的工作集上而不是在私有工作集上,这将是优化失败,因为它应该并且能够被共享。这是另一个例子:

这引出了如何确定虚拟提交的隐私性的问题。在这种情况下,它似乎将指向 CoW PPTE 的提交 PTE 分类为私有,因为当页面被写入并交换为替代页面副本时,它最终将是私有的。这是误导性的,尽管不是一个有形的问题(表明它是私有工作集的一部分将是一个有形的问题)。至于.rdata,当它在原始提交中不是私有时,它知道第一页是私有的(原始提交从提交PTE指向的PPTE的保护中确定私有性,这些PPTE是使用图像填写的部分),但奇怪的是它不包括该图像的私有页面中的 4K 页面(它显示 20K 而不是 24K),但确实包括了 CoW 私有页面。您可能认为它会读取 PPTE/PTE 以确定什么是私有提交,什么不是——当前提交与原始提交不同,因为加载程序已将只读(共享)PTE 更改为 CoW PTE和PPTE,然后它写入并使PTE只读(但PPTE仍然是CoW并保留保护,即使它指向的物理页面因文件备份和只读而被丢弃(现在它可能因为引用计数有减少),即它不会从图像文件中重新获得保护)。当页面写入时,新页面将不再涉及或再次指向PPTE,现在是pagefile backed,分配的物理页面的PFN条目不再指向PPTE。出于这个原因,共享部分是文件支持的,因为它与 PPTE 相关联,就像 CoW 页面一样,图像中的只读页面也是如此,但分配的页面与 PPTE 无关(页面 PFN不指向 PPTE) 是页面文件支持的,无论它是设置为读/写还是读(在 IAT 的情况下为读)。

在任务管理器中,缓存 = 待机 + 已修改,可用 = 空闲 + 待机。提交费用是使用的物理内存量(RAM + 页面文件)(用于提交本身(在工作集、页面文件、已修改、备用),或用于支持实际提交但不属于提交范围的结构本身,或分页/非分页池等)。

【讨论】:

    猜你喜欢
    • 2018-01-10
    • 2013-04-17
    • 2019-11-06
    • 1970-01-01
    • 2011-04-27
    • 2019-07-03
    • 1970-01-01
    • 1970-01-01
    • 2014-01-08
    相关资源
    最近更新 更多