Share via


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 (https://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

  • Anonymous
    November 28, 2005
    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! :-)

  • Anonymous
    November 28, 2005
    Take a look at .NET Memory Profiler, which does just that and much more:
    http://memprofiler.com

    PS I am not affiliated with Scitech.

  • Anonymous
    November 28, 2005
    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#. :)

  • Anonymous
    December 05, 2005
    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

  • Anonymous
    December 08, 2005
    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

  • Anonymous
    December 09, 2005
    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.


  • Anonymous
    December 09, 2005
    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

  • Anonymous
    December 11, 2005
    The comment has been removed

  • Anonymous
    March 08, 2006
    Jan Stranik is on MSDN TV talking about MDbg, the managed-debugging sample written in C#.&amp;nbsp; See the...

  • Anonymous
    May 17, 2006
    Check out: Managed Stack Explorer. It's a tool on CodePlex that lets you automatically get stack snapshots...

  • Anonymous
    June 26, 2006
    A while ago, I wrote a sample tool to gather snapshots of callstacks. After great delay, I've posted...

  • Anonymous
    December 27, 2006
    When you attach to a managed debuggee (via ICorDebug::DebugActiveProcess), ICorDebug generates a set

  • Anonymous
    July 03, 2007
    MDbg is a debugger for managed code written entirely in C# (and IL), which started shipping in the CLR

  • Anonymous
    January 15, 2008
    CLRManagedDebugger IntroductiontotheCLRManagedDebugger http://msdn.microsoft.com/msdntv/...