Search code examples
windbgunmanagedheap-dumpsos

How to find what is in unmanaged memory in Dump by WinDBG


I run for Dump File in WinDbg command

!address -summary

I results are something like this

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%

How can I find what in in Heap? What are objects or what are types?

Is Managed Heap, and Heap is managed heap?

It is very hard to ask Questions Like this, so I added more info

Here is my C# Sample Code

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();
    }
}

Here is Unmanaged Allocation Code:

using System;
using System.Runtime.InteropServices;

public class AllocateUnmanagedMemory
{

    static IntPtr pointer;

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

And results from WinDbg Preview in Windows 10

-- 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%

I should be able find somewhat that code for Unmanaged Allocation, allocated 1Gb memory.


Solution

  • The basics

    The command !address operates on a very low level, barely above the operating system. However, it will recognize a little bit of the memory manager that comes with Windows: the Windows Heap Manager.

    So, what you see as Heap that is memory which was allocated through the Windows Heap manager. On your level of understanding, that's the native heap.

    Any other heap managers will implement their own memory management. Basically they all work similar: they get large blocks of memory from VirtualAlloc() and then try to do a better magement of the small blocks within that large block. Since WinDbg doesn't know any of these memory managers, that memory is declared as <unknown>. It includes, but is not limited to the managed heap of .NET. For other potential uses, see this answer.

    Free is memory that can potentially be claimed from the operating system. This may include swap space, not only physical RAM.

    Stack, well that's obvious I think.

    The heaps

    How can I find what in in Heap? What are objects or what are types?

    The answer to this question heavily depends on which heap you're talking about.

    The Windows Heap Manager ("native heap") just manages memory and does not manage types. It's not possible on that level to distinguish two objects of the same size but different type. If you have a memory leak, you can only give a statement like "I have a leak of n bytes". To find our more about the native heap, start with !heap -s and look up the other !heap commands.

    The .NET managed heap retains a type system. To analyze the managed heap, you need an extension for WinDbg called . Usually you load it by .loadby sos clr. It has a command !dumpheap -stat which may give you a first impression of its capabilities. (Run the command twice if you get an error message)

    This should give you enough hints to do further research and find more details in your crash dump.

    Strange?

    You seem to have 230 stacks with a total of 2.5 GB of memory. That is about 11 MB of memory per stack. Usually that's limited to 1 MB.

    Your updated example code

    I compiled the following program

    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]);
            }
        }
    }
    

    I ran the application and I attached to the process with WinDbg. I took a dump using

    0:000> .dump /ma SO55043889.dmp
    

    and now we can analyze it like this:

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

    So we see 1 GB of (potentially) .NET memory and 1 GB of native memory.

    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
    

    There are 12 int[] on the .NET side, taking a total of ~1 GB from the managed heap. Looking at the details, we see that there's only one big array and some smaller ones:

    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
    

    That's not what you wanted know. I just showed you how easy it is on the .NET side.

    Now the native side:

    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      
    -----------------------------------------------------------------------------
    

    We can't see the 1 GB here. And there's a reason for that.

    As explained before, heap managers are good at dividing large blocks from VirtualAlloc() (which are 64kB) into smaller pieces. They do that because it would be a big waste to allocate 64kB just for a 4 byte int. However, there's no need to create heap management stucture for large blocks. For an allocation of 2^30+1 byte, the OS would return 2^30+64kB, which means the overhead is just 0.006%.

    That's why you will find allocations >512kB not inside the usual heap management structures but as a Virtual block, which means that the Windows Heap Manager has simply forwarded the request to VirtualAlloc().

    There's another issue here: the output for the size is broken. It says

    (size 00000000)
    

    which obviously is not true. Let's look at it ourselves:

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

    What we see here is that End Adress - Base Address equals the Region Size and the size is 1 GB.

    At this point, it's worth noting that the user mode stack trace database is useless. It only applies to items on the heap, but not VirtualAlloc(). You'll not figure out who allocated the 1 GB block.

    And I forgot to enable the user mode stack trace database anyway. Let's do that and cross check

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

    And now, there should be stack traces for smaller pieces of memory. In this example, I use an arbitrary block of size 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
    

    Just one more note: if you modify the program to have smaller blocks of memory, e.g.

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

    You will see the allocation in the heap:

    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)
    [...]
    

    And you will see stack traces

    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
    [...]
    

    But you won't see the managed method calls. That's because the USt database was built for native only. It's the same reason you have different stacks in .NET using k or !dumpstack.