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. DougAnonymous
December 05, 2011
The comment has been removedAnonymous
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).aspxAnonymous
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 PerfViewAnonymous
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 CheersAnonymous
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.