【问题标题】:How to find what is in unmanaged memory in Dump by WinDBG如何通过 WinDBG 在 Dump 中查找非托管内存中的内容
【发布时间】:2019-07-29 08:17:22
【问题描述】:

我在 WinDbg 命令中运行 Dump File

!地址-摘要

我的结果是这样的

Usage Summary   RgnCount    Total Size  %ofBusy %ofTota

    Free            3739    7ff5`dbbae000 ( 127.960 Tb)                 99.97%
    <unknown>       1677    5`680a1000 (  21.626 Gb)    53.31%       0.02%
    Heap            20349   4`0049f000 (  16.005 Gb)    39.45%       0.01%
    Stack           230 0`a3e90000 (   2.561 Gb)    6.31%        0.00%

如何在堆中找到内容?什么是对象或什么是类型?

是托管堆,堆是托管堆吗?

这样的问题很难问,所以我补充了更多信息

这是我的 C# 示例代码

class Program
{

    public static int[] arr;
    public static AllocateUnmanagedMemory cls;


    static void Main(string[] args)
    {
        const int GBSize = 1 * 1024 * 1024 * 1024/ sizeof(int);

        Console.WriteLine("Allocating");

        arr = new int[GBSize];

        cls = new AllocateUnmanagedMemory();

        cls.UnmanagedAllocation();


        Console.ReadLine();
    }
}

这是非托管分配代码:

使用系统; 使用 System.Runtime.InteropServices;

公共类 AllocateUnmanagedMemory {

static IntPtr pointer;

public void UnmanagedAllocation()
{
    pointer = Marshal.AllocHGlobal(1024 * 1024 * 1024 );
}

}

以及来自 Windows 10 中 WinDbg Preview 的结果

-- Usage Summary RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free             59          762f7000 (   1.847 GB)           46.17%
<unknown>        98          4493e000 (   1.072 GB)  49.76%   26.79%
Heap             15          40158000 (   1.001 GB)  46.50%   25.03%
Image            174           2db2000 (  45.695 MB)   2.07%    1.12%
MappedFile       15           1c51000 (  28.316 MB)   1.28%    0.69%
Stack            24            800000 (   8.000 MB)   0.36%    0.20%

我应该能够找到一些用于非托管分配的代码,分配了 1Gb 内存。

【问题讨论】:

    标签: windbg unmanaged heap-dump sos


    【解决方案1】:

    基础知识

    !address 命令在非常低的级别上运行,仅略高于操作系统。但是,它会识别一点 Windows 附带的内存管理器:Windows 堆管理器。

    所以,您看到的 Heap 是通过 Windows 堆管理器分配的内存。在您的理解水平上,这就是 本机堆

    任何其他堆管理器都将实现自己的内存管理。基本上它们的工作方式都相似:它们从VirtualAlloc() 获取大块内存,然后尝试更好地管理该大块中的小块。由于 WinDbg 不知道这些内存管理器中的任何一个,因此该内存被声明为 &lt;unknown&gt;。它包括但不限于.NET 的托管堆。有关其他潜在用途,请参阅this answer

    Free 是可能从操作系统中获得的内存。这可能包括交换空间,而不仅仅是物理 RAM。

    Stack,我认为这很明显。

    如何在堆中找到内容?什么是对象或什么是类型?

    这个问题的答案很大程度上取决于你说的是哪个堆。

    Windows 堆管理器(“本机堆”)只管理内存而不管理类型。在那个级别上,不可能区分大小相同但类型不同的两个对象。如果你有内存泄漏,你只能给出“我有 n 个字节的泄漏”之类的语句。要了解有关本机堆的更多信息,请从 !heap -s 开始,然后查找其他 !heap 命令。

    .NET 托管堆保留一个类型系统。要分析托管堆,您需要一个名为 的 WinDbg 扩展。通常您通过.loadby sos clr 加载它。它有一个命令!dumpheap -stat,它可以让你对它的功能有个第一印象。 (如果收到错误消息,请运行该命令两次)

    这应该会为您提供足够的提示,以便您进行进一步研究并在故障转储中找到更多详细信息。

    奇怪吗?

    您似乎有 230 个堆栈,总内存为 2.5 GB。每个堆栈大约有 11 MB 的内存。通常限制为 1 MB。

    您更新的示例代码

    我编译了以下程序

    using System;
    using System.Runtime.InteropServices;
    namespace SO55043889
    {
        class Program
        {
            public static int[] arr;
            static IntPtr pointer;
            static void Main()
            {
                const int GBSize = 1 * 1024 * 1024 * 1024/ sizeof(int);
                Console.WriteLine("Allocating");
                arr = new int[GBSize];
                pointer = Marshal.AllocHGlobal(1024 * 1024 * 1024 );
                Console.ReadLine();
                Console.WriteLine(pointer.ToInt32() + arr[0]);
            }
        }
    }
    

    我运行了应用程序,并使用 WinDbg 附加到了该进程。我使用了转储

    0:000> .dump /ma SO55043889.dmp
    

    现在我们可以这样分析:

    0:000> !address -summary
    [...]
    <unknown>                               106          474f4000 (   1.114 GB)  51.58%   27.86%
    Heap                                     13          401e1000 (   1.002 GB)  46.38%   25.05%
    [...]
    

    所以我们看到 1 GB(可能).NET 内存和 1 GB 本机内存。

    0:000> .loadby sos clr
    0:000> !dumpheap -stat
    c0000005 Exception in C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dumpheap debugger extension.
      PC: 04f6fa73  VA: 00000000  R/W: 0  Parameter: 00000000
    0:000> *** This is normal, just do it again
    0:000> !dumpheap -stat
    [...]
    70d20958       12   1073742400 System.Int32[]
    Total 335 objects
    

    .NET 端有 12 个 int[],从托管堆中总共占用约 1 GB。查看细节,我们看到只有一个大数组和一些较小的数组:

    0:000> !dumpheap -type System.Int32[]
     Address       MT     Size
    020e1ff8 70d20958      300     
    020e2130 70d20958       24     
    020e2184 70d20958       40     
    020e2228 70d20958       80     
    020e2d9c 70d20958       16     
    020e2dac 70d20958       16     
    020e2df8 70d20958       16     
    020e386c 70d20958       24     
    020e3d54 70d20958       16     
    020e3d64 70d20958       16     
    020e3d74 70d20958       16     
    04811010 70d20958 1073741836     
    
    Statistics:
          MT    Count    TotalSize Class Name
    70d20958       12   1073742400 System.Int32[]
    Total 12 objects
    

    这不是你想知道的。我刚刚向您展示了它在 .NET 方面是多么容易。

    现在是原生端:

    0:004> !heap -s
    LFH Key                   : 0x7f8d0cc6
    Termination on corruption : ENABLED
      Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                        (k)     (k)    (k)     (k) length      blocks cont. heap 
    -----------------------------------------------------------------------------
    Virtual block: 80010000 - 80010000 (size 00000000)
    00550000 00000002    1024    504   1024     14    17     1    1      0   LFH
    002d0000 00001002      64     16     64      2     2     1    0      0      
    00820000 00041002     256      4    256      2     1     1    0      0      
    00750000 00001002      64     20     64      7     2     1    0      0      
    00710000 00001002     256      4    256      0     1     1    0      0      
    001e0000 00041002     256      4    256      2     1     1    0      0      
    -----------------------------------------------------------------------------
    

    我们在这里看不到 1 GB。这是有原因的。

    如前所述,堆管理器擅长将大块从VirtualAlloc()(64kB)分割成更小的块。他们这样做是因为只为 4 字节 int 分配 64kB 会造成很大的浪费。但是,没有必要为大块创建堆管理结构。对于 2^30+1 字节的分配,操作系统将返回 2^30+64kB,这意味着开销仅为 0.006%。

    这就是为什么您会发现大于 512kB 的分配不在通常的堆管理结构中,而是作为 Virtual block,这意味着 Windows 堆管理器只是将请求转发到 VirtualAlloc()

    这里还有另一个问题:size 的输出已损坏。它说

    (size 00000000)
    

    这显然不是真的。我们自己看吧:

    0:004> !address 80010000 
        Usage:                  Heap
        Base Address:           80010000
        End Address:            c0011000
        Region Size:            40001000
        [...]
    
        0:004> ? c0011000-80010000
        Evaluate expression: 1073745920 = 40001000
    

    我们在这里看到的是End Adress - Base Address 等于Region Size,大小为 1 GB。

    此时,值得注意的是,用户模式堆栈跟踪数据库是无用的。它仅适用于堆上的项目,但不适用于VirtualAlloc()。你不会知道是谁分配了 1 GB 块。

    我还是忘了启用用户模式堆栈跟踪数据库。让我们这样做并交叉检查

    0:000> !gflag
    Current NtGlobalFlag contents: 0x00001000
    ust - Create user mode stack trace database
    

    现在,应该有小块内存的堆栈跟踪。在这个例子中,我使用了一个任意大小为 0x208 的块:

    0:000> !heap -flt s 208
        _HEAP @ 2a0000
          HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
            002c9818 0044 0000  [00]   002c9830    00208 - (busy)
            002cd1e8 0044 0044  [00]   002cd200    00208 - (busy)
            002d5ad0 0044 0044  [00]   002d5ae8    00208 - (busy)
            002f0c48 0044 0044  [00]   002f0c60    00208 - (busy)
            0032c210 0044 0044  [00]   0032c228    00208 - (busy)
            00351c90 0044 0044  [00]   00351ca8    00208 - (busy)
    0:000> *** Use any UserPtr number, I use the last one
    0:000> !heap -p -a 00351ca8    
        address 00351ca8 found in
        _HEAP @ 2a0000
          HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
            00351c90 0044 0000  [00]   00351ca8    00208 - (busy)
            779dd909 ntdll!RtlAllocateHeap+0x00000274
            71e18bc7 clr!EEHeapAlloc+0x0000002c
            71e18c0a clr!EEHeapAllocInProcessHeap+0x0000005b
            71e18ba6 clr!ClrAllocInProcessHeap+0x00000023
            71e2dd26 clr!StackingAllocator::AllocNewBlockForBytes+0x00000082
            71e2dd76 clr!operator new+0x00000063
            71e93ace clr!MethodTableBuilder::BuildMethodTableThrowing+0x00000059
            71e94590 clr!ClassLoader::CreateTypeHandleForTypeDefThrowing+0x0000083a
            71e2e956 clr!ClassLoader::CreateTypeHandleForTypeKey+0x000000ad
            71e2e99a clr!ClassLoader::DoIncrementalLoad+0x000000c2
            71e2e418 clr!ClassLoader::LoadTypeHandleForTypeKey_Body+0x00000505
            71e2e5a7 clr!ClassLoader::LoadTypeHandleForTypeKey+0x000000b5
            71e2f723 clr!ClassLoader::LoadTypeDefThrowing+0x00000318
            71e2a974 clr!ClassLoader::LoadTypeDefOrRefThrowing+0x0000024c
            71f57811 clr!Assembly::GetEntryPoint+0x0000022f
            71f856e0 clr!Assembly::ExecuteMainMethod+0x000000b3
            71f855ed clr!SystemDomain::ExecuteMainMethod+0x00000631
            71f858d3 clr!ExecuteEXE+0x0000004c
            71f85819 clr!_CorExeMainInternal+0x000000dc
            71f55a0c clr!_CorExeMain+0x0000004d
            7251d93b mscoreei!_CorExeMain+0x0000010e
            72597f16 MSCOREE!ShellShim__CorExeMain+0x00000099
            72594de3 MSCOREE!_CorExeMain_Exported+0x00000008
            77999802 ntdll!__RtlUserThreadStart+0x00000070
            779997d5 ntdll!_RtlUserThreadStart+0x0000001b
    

    还有一点需要注意:如果您修改程序以拥有更小的内存块,例如

    for (int i = 0; i < 1000; i++)
    {
        pointer = Marshal.AllocHGlobal(3*1024 );
    }
    

    你会看到堆中的分配:

    0:004> ? 3*0n1024
    Evaluate expression: 3072 = 00000c00
    0:004> !heap -flt c00
    cound not parse flt criteria -flt c00
    0:004> !heap -flt s c00
        _HEAP @ 67c0000
          HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
            0686b668 0183 0000  [00]   0686b680    00c00 - (busy)
            0686efa8 0183 0183  [00]   0686efc0    00c00 - (busy)
    [...]
    

    你会看到堆栈跟踪

    0:004> !heap -p -a 4d0fdf18    
        address 4d0fdf18 found in
        _HEAP @ 67c0000
          HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
            4d0fdf00 0191 0000  [00]   4d0fdf18    00c00 - (busy)
            779dd909 ntdll!RtlAllocateHeap+0x00000274
            768f5aae KERNELBASE!LocalAlloc+0x0000005f
            70c6ad4f mscorlib_ni+0x003fad4f
            7138c4da mscorlib_ni+0x00b1c4da
            71e0ebb6 clr!CallDescrWorkerInternal+0x00000034
            71e11e10 clr!CallDescrWorkerWithHandler+0x0000006b
            71e17994 clr!MethodDescCallSite::CallTargetWorker+0x0000016a
            71f85026 clr!RunMain+0x000001ad
            71f85707 clr!Assembly::ExecuteMainMethod+0x00000124
    [...]
    

    但您不会看到托管方法调用。那是因为 USt 数据库是为本地构建的。这与使用 k!dumpstack 在 .NET 中拥有不同堆栈的原因相同。

    【讨论】:

    • 我添加了示例代码,您的回答绝对正确,如果我们不知道 中的内容,它只是字节。但实际上,我们总是引用一些 DLL,以及分配非托管内存的代码,很难识别这些 DLL 和代码。这就是我正在寻找的。在示例中查找谁分配了 1GB 代码。(或在第一个示例中为 14 Gb)
    • 所以它有效!我迫不及待地想检查它的真实生活。非常感谢!
    • @justromagod:我刚刚添加了另一个关于 NET 堆栈的注释
    • 好像没有启用gflag,我所有的转储都没用。
    • @justromagod:是的
    猜你喜欢
    • 1970-01-01
    • 2013-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-14
    • 2011-02-08
    • 2020-12-12
    相关资源
    最近更新 更多