Tool to get snapshot of managed callstacks


I wrote a simple tool to take a snapshot of a running managed process and dump the output as an XML file. I’ll post the full source as a sample on MSDN.
[Update 6/26/06] After great delay, source posted here. Also, check out Managed Stack Explorer, which is a more polished tool that has similar snap-shot gathering behavior.


The usage is pretty simple. To take a snapshot of the running process “hello.exe”, run:
    SnapShot.exe -name:hello.exe


And then it dumps out an XML containing callstacks of all threads, including locals and arguments of each frame (see below). 


Comments on the tool:
The actual tool is trivial to write. It’s under 500 lines, and the largest part is adding error checking for the command line options and breaking everything out into little xml tags. Here’s a watered down basic version of the tool which just attaches (see here for details on attach) and dumps callstacks via MDbgFrame.ToString() (eg, the equivalent of MDbg’s where command), and it’s under 70 C# lines (Update: fix an issue with draining attach events, bumps the line count up from 50 to 70):




//—————————————————————————–
// Harness to snapshot a process’s callstacks
// Built on MDbg, Needs a reference to MdbgCore.dll (ships in CLR 2.0 SDK).
// Author: Mike Stall (http://blogs.msdn.com/jmstall)
//—————————————————————————–

using System;
using
Microsoft.Samples.Debugging.MdbgEngine;
using
System.Diagnostics;

class Program
{
// Skip past fake attach events.
static void DrainAttach(MDbgEngine debugger, MDbgProcess
proc)
{
bool
fOldStatus = debugger.Options.StopOnNewThread;
debugger.Options.StopOnNewThread =
false;
// skip while waiting for AttachComplete

proc.Go().WaitOne();
Debug.Assert(proc.StopReason is AttachCompleteStopReason
);

debugger.Options.StopOnNewThread = true; // needed for attach= true; // needed for attach

// Drain the rest of the thread create events.
while (proc.CorProcess.HasQueuedCallbacks(null
))
{
proc.Go().WaitOne();
Debug.Assert(proc.StopReason is ThreadCreatedStopReason
);
}

debugger.Options.StopOnNewThread = fOldStatus;
}

// Expects 1 arg, the pid as a decimal string
static void Main(string
[] args)
{
int pid = int.Parse(args[0]);
MDbgEngine debugger = new MDbgEngine();

MDbgProcess proc = null;
try
{
proc = debugger.Attach(pid);
DrainAttach(debugger, proc);

MDbgThreadCollection tc = proc.Threads;
Console.WriteLine(“Attached to pid:{0}”
, pid);
foreach (MDbgThread t in
tc)
{
Console.WriteLine(“Callstack for Thread {0}”
, t.Id.ToString());

foreach (MDbgFrame f in t.Frames)
{
Console.WriteLine(” “
+ f);
}
}
}
finally
{
if (proc != null
) { proc.Detach().WaitOne(); }
}

}
}


Some sample output from that is:



Attached to pid:3784
Callstack for Thread 2384
  [Internal Frame, ‘M–>U’]
  System.IO.__ConsoleStream.ReadFileNative (source line information unavailable)
  System.IO.__ConsoleStream.Read (source line information unavailable)
  System.IO.StreamReader.ReadBuffer (source line information unavailable)
  System.IO.StreamReader.ReadLine (source line information unavailable)
  System.IO.TextReader.SyncTextReader.ReadLine (source line information unavailable)
  System.Console.ReadLine (source line information unavailable)
  Foo.Main (wait.cs:15)


Some technical notes:
This is doing an invasive attach, running the callstacks, and then doing a detach.  It is not taking a memory dump. Only 1 managed debugger can attach at a time (see here), and you can’t do this if a native debugger is already attached (see here). The MDbgProcess.Detach() call is in a finally such that if the harness does crash in the middle, then it will at least detach from the target app.


Lines of Code vs. Functionality: C# vs. Mdbg script:
You could do the same thing with an MDbg script like:
    attach %1
    for where
    detach


Where %1 is the pid of interest.
This is a cute tangent about lines of code vs. functionality:
3 lines of Mdbg script provide the raw functionality of attach, get the callstacks, and detach. Though you don’t get control over formatting, and it doesn’t scale well to doing things differently.
We go up to 70 lines of C# to be able to run it from a C# harness without pulling in MDbg.exe proper.
We go up to 500 lines of C# once we add dumping values, a few fancy options (attach by name), error checks, and spew to XML instead of just using the default ToString().


Sample output:


Sample output from the real XML-based tool looks like this (I removed some redundant frames).  It has an arbitrarily policy to dump the first depth of fields for reference types. I plan to publish the source for this tool somewhere on MSDN.


<!–Snapshot of managed process taken from SnapShot gathering tool (built on MDbg).–>
<
process pid=2144>
<
thread tid=3252>
<
callstack>
<
frame hint=C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll!System.IO.TextReader.SyncTextReader.ReadLine (source line information unavailable) il=0 mapping=MAPPING_UNMAPPED_ADDRESS>
<
locals />
<
arguments>
<
value name=this type=System.IO.TextReader.SyncTextReader>
<
fields>
<
value name=_in type=System.IO.StreamReader>System.IO.StreamReader</value>
<
value name=Null type=System.IO.TextReader><null></value>
<
value name=__identity type=System.Object><null></value>
</
fields>
</
value>
</
arguments>
</
frame>
<
frame hint=C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll!System.Console.ReadLine (source line information unavailable) il=0 mapping=MAPPING_EPILOG>
<
locals />
<
arguments />
</
frame>
<
frame hint=C:\bugs\hello.exe!t.Main (hello.cs:133) il=273>
<
locals>
<
value name=CS$1$0000 type=System.Int32>0</value>
<
value name=CS$4$0001 type=System.Boolean>False</value>
<
value name=x type=System.Int32>3</value>
<
value name=t2 type=t>
<
fields>
<
value name=MyString type=System.String>“hi!”</value>
<
value name=m_x type=System.Int32>0</value>
</
fields>
</
value>
<
value name=tInt type=System.RuntimeType>
<
fields>
<
value name=m_cache type=System.IntPtr>0</value>
<
value name=m_handle type=System.RuntimeTypeHandle>System.RuntimeTypeHandle</value>
<
value name=s_typeCache type=System.RuntimeType.TypeCacheQueue><null></value>
<
value name=s_typedRef type=System.RuntimeType>System.RuntimeType</value>
<
value name=s_ActivatorCache type=System.RuntimeType.ActivatorCache><null></value>
<
value name=s_ForwardCallBinder type=System.OleAutBinder><null></value>
<
value name=FilterAttribute type=System.Reflection.MemberFilter><null></value>
<
value name=FilterName type=System.Reflection.MemberFilter><null></value>
<
value name=FilterNameIgnoreCase type=System.Reflection.MemberFilter><null></value>
<
value name=Missing type=System.Object><null></value>
<
value name=Delimiter type=System.Char>\0</value>
<
value name=EmptyTypes type=System.Type[]><null></value>
<
value name=defaultBinder type=System.Object><null></value>
<
value name=valueType type=System.Type><null></value>
<
value name=enumType type=System.Type><null></value>
<
value name=objectType type=System.Type><null></value>
<
value name=m_cachedData type=System.Reflection.Cache.InternalCache><null></value>
</
fields>
</
value>
<
value name=fp1 type=t.FP1>
<
fields>
<
value name=_invocationList type=System.Object><null></value>
<
value name=_invocationCount type=System.IntPtr>0</value>
<
value name=_target type=t.FP1>t.FP1</value>
<
value name=_methodBase type=System.Reflection.MethodBase><null></value>
<
value name=_methodPtr type=System.IntPtr>3416108</value>
<
value name=_methodPtrAux type=System.IntPtr>9515312</value>
</
fields>
</
value>
<
value name=q type=t[]>array [2]</value>
<
value name=s1 type=System.String>“abc”</value>
<
value name=s2 type=System.String>“abc”</value>
</
locals>
<
arguments>
<
value name=args type=System.String[]>array [1]</value>
</
arguments>
</
frame>
</
callstack>
</
thread>
</
process>
 

Comments (14)

  1. Uwe Keim says:

    Wow, finally some tool to get call stack WITH the current values of the parameters passed to this function.

    I was looking long time for something like this. Thanks! 🙂

  2. Sergey M says:

    Take a look at .NET Memory Profiler, which does just that and much more:

    http://memprofiler.com

    PS I am not affiliated with Scitech.

  3. Sergey – .NET Memory Profiler looks very cool. Thanks for the link.

    Two things:

    – I don’t know if it lets you *attach* to a running app (.NET Profiling APIs don’t allow that).

    – It’s not 45 lines of C#. 🙂

  4. Sam says:

    A very cool addition would be to allow the app to collect cpu usage stats for each thread for a defined period of time, and output it with the dump. That way if thread X is taking 100% CPU you could very quick / automatically pick up the offending thread and find the area in code that caused the problems.

    It can be done now with process explorer and mdbg , but it would be nice to be able to do this from 1 app

  5. wolfgang hauer says:

    Do you think that the snapshot logic could be integrated in an application so that when i have an red alert i can dump to an xml-file

    tks wolfgang

  6. Wolfgang – yes.

    Note that you can’t debug yourself (see http://blogs.msdn.com/jmstall/archive/2005/11/05/cant_debug_yourself.aspx ), but you could spawn a worker app to take the snapshot for you.

  7. wolfgang hauer says:

    tks – that makes me happy for killing some bugs in my app.

    You mentioned, that you will present the code on msdn. Is the utility exe or source somewhere available earlier ? ( as a pre-cristmas gift)

    wolfgang

  8. Wolfgang – I’m curious how this will help kill some bugs? Do you have a URL?

    I hope to get the sample up soon.

  9. Jan Stranik is on MSDN TV talking about MDbg, the managed-debugging sample written in C#.&amp;nbsp; See the…

  10. Check out: Managed Stack Explorer. It’s a tool on CodePlex that lets you automatically get stack snapshots…

  11. A while ago, I wrote a sample tool to gather snapshots of callstacks. After great delay, I’ve posted…

  12. When you attach to a managed debuggee (via ICorDebug::DebugActiveProcess), ICorDebug generates a set

  13. MDbg is a debugger for managed code written entirely in C# (and IL), which started shipping in the CLR

  14. Warren Tang says:

    CLRManagedDebugger IntroductiontotheCLRManagedDebugger

    http://msdn.microsoft.com/msdntv/