Compartilhar via


Determining the origin of a static root

A number of times when debugging managed code I've realised an object is ultimitely rooted in a static member but I've not been sure how to determine where in the application that static is declared. Today I finally got round to figuring out a way to do it.

Take this program as an example:

 using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static ArrayList _fruit = new ArrayList();
        static void Main(string[] args)
        {
            _fruit.Add(new Fruit("Apple"));
            _fruit.Add(new Fruit("Bannana"));
            _fruit.Add(new Fruit("Cherry"));
            Console.ReadLine();
        }

    }

    class Fruit
    {
        public Fruit(string Name)
        {
            _name = Name;
        }
        private string _name = "";
    }
}

Suppose I am debugging this and want to determine where these Fruit instances are rooted. Obviously in this case I know 'cause I just wrote the program but let's pretend I don't.

First we need to load the SOS extension for managed debugging:

 0:003> .loadby sos mscorwks

Note that the above is for Whidbey SOS debugging. For 1.1 framework debugging you need to load the SOS that installs with WinDBG:

 0:003> .load clr10\sos

Next we need to find where one of the Fruit instances is rooted.

 0:003> !dumpheap -type Fruit
 Address       MT     Size
02651c28 01cc30c4       12     
02651c68 01cc30c4       12     
02651c74 01cc30c4       12     
total 3 objects
Statistics:
      MT    Count    TotalSize Class Name
01cc30c4        3           36 ConsoleApplication1.Fruit
Total 3 objects
0:003> !gcroot 02651c28 
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread c8c
ESP:2cf438:Root:02651ba4(System.Collections.ArrayList)->
02651c48(System.Object[])->
02651c28(ConsoleApplication1.Fruit)
ESP:2cf450:Root:02651ba4(System.Collections.ArrayList)->
02651c48(System.Object[])
ESP:2cf464:Root:02651ba4(System.Collections.ArrayList)->
02651c48(System.Object[])
Scan Thread 2 OSTHread d98
DOMAIN(002D9968):HANDLE(Pinned):1ca13fc:Root:03651010(System.Object[])->
02651ba4(System.Collections.ArrayList)

So we can see that in addition to some roots in registers on the current thread, it is rooted in a pinned Object[]. Let's look closer at the Object array:

 0:003> !do 03651010
Name: System.Object[]
MethodTable: 79124228
EEClass: 7912479c
Size: 4096(0x1000) bytes
Array: Rank 1, Number of elements 1020, Type CLASS
Element Type: System.Object
Fields:
None

The size of this array is 1020 elements and 0x1000 bytes. You'll see such arrays quite often when debugging managed code because the static members for an AppDomain are held in an Object array. Each static is represented by a particular element in this array. So we can find it by searching for it in the memory occupied by the Object[]:

 0:003> s-d 03651010 L?0x1000 02651ba4
03651ec4  02651ba4 00000000 00000000 00000000  ..e.............

So the address of the element of the array that holds the statics is 0x03651ec4. Let's search the whole of memory for references to that address:

 0:003> s-d 0 L?0xbfffffff 03651ec4  
01cc2fc8  03651ec4 00000500 01cc3008 11000001  ..e......0......
02060094  03651ec4 e13989e8 c35e9077 01cc1880  ..e...9.w.^.....

Luckily we only found two hits. Check if any of these are in JITted code:

 0:003> !u 01cc2fc8  
Unmanaged code
01cc2fc8 c41e             les     ebx,[esi]
01cc2fca 650300           add     eax,gs:[eax]
01cc2fcd 0500000830       add     eax,0x30080000
01cc2fd2 cc               int     3
01cc2fd3 0101             add     [ecx],eax
01cc2fd5 0000             add     [eax],al
01cc2fd7 1100             adc     [eax],eax
01cc2fd9 0000             add     [eax],al
01cc2fdb 90               nop
01cc2fdc 0000             add     [eax],al

NO, that's not it. Check the next one:

 0:003> !u 02060094  
Normal JIT generated code
ConsoleApplication1.Program..cctor()
Begin 02060070, size 30
02060070 56               push    esi
02060071 833dc82dcc0100   cmp     dword ptr [01cc2dc8],0x0
02060078 7405             jz      0206007f
0206007a e87f220378       call    mscorwks!JIT_DbgIsJustMyCode (7a0922fe)
0206007f b9b0361079       mov     ecx,0x791036b0 (MT: System.Collections.ArrayList)
02060084 e8931fc5ff       call    01cb201c (JitHelp: CORINFO_HELP_NEWSFAST)
02060089 8bf0             mov     esi,eax
0206008b 8bce             mov     ecx,esi
*** WARNING: Unable to verify checksum for mscorlib.ni.dll
0206008d e8ce1d3077       call    mscorlib_ni+0x2a1e60 (79361e60) (System.Collections.ArrayList..ctor(), mdToken: 0600153c)
02060092 8d15c41e6503     lea     edx,[03651ec4]
02060098 e88939e177    call mscorwks!JIT_Writeable_Thunks_Buf+0xf6 (79e73a26) (mscorwks!JIT_Writeable_Thunks_Buf)
0206009d 90               nop
0206009e 5e               pop     esi
0206009f c3               ret

This is it! In the static constructor of our Program class (ConsoleApplication1.Program..cctor) we create the new ArrayList. This is then stored in the Object[].

Comments

  • Anonymous
    January 31, 2010
    Hello Doug. I've just used your helpful instructions to find a static string[0x10000], hidden in a huge codebase. Many Thanks. Tal Rosen.

  • Anonymous
    January 31, 2010
    Great! I'm pleased to hear that. Thanks for taking the time to give the feedback. Doug

  • Anonymous
    December 05, 2011
    The comment has been removed

  • Anonymous
    November 04, 2014
    There was one command s-q on one of the sites. What is the difference between s-d and s-q? I could not find much explanation on Google.

  • Anonymous
    November 12, 2014
    Hello Prashant, s-q searches for a QWORD (64 bits) and s-d searches for a DWORD (32 bits). All the flags for the s command are covered here: msdn.microsoft.com/.../ff558855(v=vs.85).aspx

  • Anonymous
    November 08, 2015
    Hey Doug - I'm getting no hits when I search the entire memory range for the address I find when running this command: s-d 0 L?0xbfffffff <address> Is there any particular reason why it would come up blank? Is there an alternative way to out which structure is at this address?

  • Anonymous
    November 08, 2015
    Hello Vinny, wow this blog post is coming up for its 10th anniversary - glad to see it is still of some use.  One reason could be if you are dealing with a 64 bit process. That command worked ok for 32 bit but for a 64-bit process you would only be searching a small part of the address space. Unfortunately generalising that to 64-bit is something I've never found easy.  Best I have come up with to date for searching all committed address space is to make use of aspects of !address. For example this searches private committed regions for the word "Surface" - you would have to adapt it for the above - !address -f:MEM_PRIVATE,MEM_COMMIT -c:"s %1 L?%3  'S' 'u' 'r' 'f' 'a' 'c' 'e' 'C' " -e There may be better ways to achieve whatever your are trying to achieve. e.g. if tracking down memory leaks take a look at PerfView

  • Anonymous
    November 09, 2015
    Thanks Doug. Seems as though .NET memory leak analysis hasn't matured much in the last decade so we're all still rooting around memory addresses with WinDBG! I'm using a 32-bit process in a 32 browser on a 64 bit machine (it's actually a 32-bit Silverlight application of all things). The addresses I'm working with aren't prepended with a lot of 0000'a so i figure i'm working in a 32 bit address space. I saw a post on stackoverflow that commented about how the approach you mentioned wont work for .NET 2.0+ applications? Any idea if that's correct? Seems as though you cover both in  the tutorial above. See first comment in the first answer to this question - stackoverflow.com/.../windbg-sos-finding-which-class-has-static-reference-to-object Cheers

  • Anonymous
    November 10, 2015
    It is almost certainly correct that I wrote the post based on .NET 1.1. And it could be it does not work for 2.0 and later.  I've not really had cause to go back and look at this in detail since. There are usually other ways and means to figure out the cause of the leak. Tracing to the root may tell you the precise "why" it is still referenced. But usually once you've figured out the main thing that is leaking, you figure out from there where you allocate it then get the "ahah" moment as to why it is ending up being held alive.

  • Anonymous
    September 19, 2017
    Great. Thanks.