//-----------------------------------------------------------------------------
// Harness to snapshot a process's callstacks and some variables.
// Built on MDbg.
// Needs a reference to MdbgCore.dll (ships in CLR 2.0 SDK).
//
// Author: Mike Stall (https://blogs.msdn.com/jmstall)
// More info: https://blogs.msdn.com/jmstall/archive/2005/11/28/snapshot.aspx
//-----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using Microsoft.Samples.Debugging.MdbgEngine;
using System.IO;
using System.Xml;
using Microsoft.Samples.Debugging.CorDebug.NativeApi;
using System.Text.RegularExpressions;
namespace Snapshot
{
class Program
{
// Helper to get options.
// this may also do some work to resolve options (and so it's not just a trivial container).
// The options handed back are not necessarily verified to be valid (eg, pid may be bogus).
class Options
{
// Print the usage mesasge
static void PrintUsage()
{
Console.WriteLine("Snapshot - takes a snapshot (callstacks w/ locals on all threads) and");
Console.WriteLine(" writes out to an XML file. This will do an INVASIVE attach (attaches");
Console.WriteLine(" as a managed debugger, does the inspection, and then detaches)");
Console.WriteLine(" Parameters:");
Console.WriteLine(" -out:<filename> | specify filename of output xml file. (Default:out.xml)");
Console.WriteLine(" -global:<var> | captures global variable. Use C# syntax");
Console.WriteLine(" | eg, 'module.exe!Class.StaticField'");
Console.WriteLine(" -pid:<pid> | attach to the given pid.");
Console.WriteLine(" -name:<name> | attach to app with the given shortname");
Console.WriteLine(" -? | prints this message.");
}
// Helper to parse an option string like "-option:value" and get the parts from it.
static void GetParts(string arg, out string option, out string value)
{
Regex regex = new Regex(@"[-/](.+?):(.+)", RegexOptions.Singleline);
Match m = regex.Match(arg);
if (!m.Success)
{
throw new OptionException("Illegal argument:" + arg);
}
option = m.Groups[1].Value;
value = m.Groups[2].Value;
}
// Constructor
// Creates an Options class by parsing command-line options.
public Options(string[] args)
{
try
{
Worker(args);
// Set defaults
if (m_outFile == null)
{
m_outFile = "out.xml";
}
}
catch (System.ApplicationException e)
{
// Illegal option. Print error now and exit.
Console.WriteLine();
ConsoleColor c = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(e.Message);
Console.ForegroundColor = c;
Console.WriteLine();
PrintUsage();
Environment.Exit(1);
}
}
// Do the real work of parsing through the options.
// Throws an ApplicationException on any errors.
void Worker(string[] args)
{
if ((args == null) || (args.Length == 0))
{
throw new OptionException("Print help"); // print help
}
foreach (string arg in args)
{
if (arg == "-?" || arg == "/?")
{
throw new OptionException("User requested help.");
}
string option;
string value;
GetParts(arg, out option, out value);
switch (option)
{
case "out":
if (m_outFile != null)
{
throw new OptionException("Can't use '-out' twice. First value was '" + m_outFile + "'. Can't set to '" + value + "'");
}
m_outFile = value;
break;
case "global":
m_globals.Add(value);
break;
case "pid":
try
{
AssignPid(int.Parse(value, System.Globalization.NumberStyles.AllowHexSpecifier));
}
catch (FormatException)
{
throw new OptionException("Pids must be a decimal or hex number. Value '" + value + "' is an illegal pid.");
}
break;
case "name":
AssignPidByName(value);
break;
default:
throw new OptionException("Unrecognized option:" + option);
}
}
}
// Assign the pid based off the method name.
void AssignPidByName(string value)
{
// Given the command line args, determine the target pid.
// This could look by friendly name or by exact pid match (decimal or hex).
// Chop off extension to get short name.
Regex r = new Regex(@"\.exe");
string shortName = r.Replace(value, "", 1);
// find by name
Process[] list = Process.GetProcessesByName(shortName); // takes short name
if (list.Length == 0)
{
throw new OptionException("No process of name '" + value + "'");
}
if (list.Length > 1)
{
throw new OptionException("Multiple processes of name '" + value + "'. Use -pid to disambiguate.");
}
Process p = list[0];
int pid = p.Id;
AssignPid(pid);
}
// Assign the pid and check for uniqueness
void AssignPid(int pid)
{
if (m_pid != 0)
{
throw new OptionException("Can't attach to multiple targets (can only use -pid or -name once).");
}
m_pid = pid;
}
#region Properties
// the XML file to dump all the information to.
public string OutputXmlFile
{
get { return m_outFile; }
}
string m_outFile;
// A list of globals to capture. Never null (though it may be an enumerator with 0 items).
public IList<string> Globals
{
get { return m_globals; }
}
List<string> m_globals = new List<string>();
// The Pid to attach the harness to.
public int Pid
{
get { return m_pid; }
}
int m_pid;
#endregion Properties
// Don't use ApplicationExceptionDirectly to avoid violating:
// https://www.gotdotnet.com/team/fxcop/docs/rules.aspx?version=1.32&url=/Usage/DoNotRaiseReservedExceptionTypes.html
class OptionException : ApplicationException
{
public OptionException(string message)
:
base(message)
{
}
}
} // end Options Class
static void Main(string[] args)
{
// Need to determine which process
Options opt = new Options(args);
Console.WriteLine("Debugging process pid={0}", opt.Pid);
MDbgEngine debugger = new MDbgEngine();
// Get a Text Writer to spew the PDB to.
XmlDocument doc = new XmlDocument();
XmlWriter xw = doc.CreateNavigator().AppendChild();
xw.WriteStartDocument();
xw.WriteComment("Snapshot of managed process taken from SnapShot gathering tool (built on MDbg).");
{
xw.WriteStartElement("process");
xw.WriteAttributeString("pid", opt.Pid.ToString());
MDbgProcess proc = null;
try
{
proc = debugger.Attach(opt.Pid);
DrainAttachEvents(debugger, proc);
// Dump custom global data.
foreach (string global in opt.Globals)
{
DumpGlobalValue(proc, global, xw);
}
DumpAllThreads(proc, xw);
}
finally
{
// We always want to detach from target.
if (proc != null)
{
proc.Detach().WaitOne();
}
}
}
xw.WriteEndDocument();
xw.Close();
doc.Save(opt.OutputXmlFile);
Console.WriteLine("Done with detach");
}
#region Find Hack Frame
// This is an evil hack to work around a bug in the MDbg layer.
// MDbgProcess.ResolveVariable needs a non-null MDbgFrame object that it can resolve vars on.
// This is conceptually not needed to resolve globals (which is what we're looking for).
// So we search through and find a usable frame.
private static MDbgFrame FindHackFrameWorker(MDbgProcess proc)
{
foreach (MDbgThread t in proc.Threads)
{
foreach (MDbgFrame f in t.Frames)
{
try
{
// Throws an exception if invalid.
f.Function.GetArguments(f);
// Frame can be used to resolve variables. Done with search.
return f;
}
catch
{
}
}
}
return null;
}
// Cache result of FindHackFrameWorker
private static MDbgFrame FindHackFrame(MDbgProcess proc)
{
if (m_cachedHackFrameValid != proc)
{
m_cachedHackFrame = FindHackFrameWorker(proc);
m_cachedHackFrameValid = proc;
}
return m_cachedHackFrame;
}
static MDbgProcess m_cachedHackFrameValid;
static MDbgFrame m_cachedHackFrame;
#endregion Find Hack Frame
#region Dump to XML
// dump value of custom globals.
private static void DumpGlobalValue(MDbgProcess proc, string globalValue, XmlWriter xw)
{
// This is an insane hack
MDbgFrame f = FindHackFrame(proc);
if (f == null)
{
xw.WriteComment("Can't find resolution frame for global:" + globalValue);
return;
}
MDbgValue v = proc.ResolveVariable(globalValue, f);
if (v == null)
{
xw.WriteComment("Can't resolve global:" + globalValue);
}
else
{
xw.WriteStartElement("global");
DumpValue(v, xw);
xw.WriteEndElement(); // "global";
}
}
// Dump all callstacks on all threads. For each callstack, dump all locals + parameters.
static void DumpAllThreads(MDbgProcess proc, XmlWriter xw)
{
MDbgThreadCollection tc = proc.Threads;
foreach (MDbgThread t in tc)
{
xw.WriteStartElement("thread");
xw.WriteAttributeString("tid", t.Id.ToString());
xw.WriteStartElement("callstack");
foreach (MDbgFrame f in t.Frames)
{
DumpFrame(f, xw);
}
xw.WriteEndElement(); // callstack
xw.WriteEndElement(); // thread
}
}
// Dump a single frame.
static void DumpFrame(MDbgFrame f, XmlWriter xw)
{
// Don't need "Information Only" Frames (ICorDebugInternalFrame).
if (f.IsInfoOnly)
{
return;
}
try
{
xw.WriteStartElement("frame");
{
// This will print the frame name and other random data.
// This is not structured data, so we call it a "hint".
// We could add mroe XML writes to print this as structured data.
string stModule = f.Function.Module.CorModule.Name;
xw.WriteAttributeString("hint", stModule + '!' + f.ToString());
{
// Print IL offset.
uint ip;
CorDebugMappingResult result;
f.CorFrame.GetIP(out ip, out result);
xw.WriteAttributeString("il", ip.ToString());
if (result != CorDebugMappingResult.MAPPING_EXACT)
{
xw.WriteAttributeString("mapping", result.ToString());
}
}
// WriteLocals
try
{
xw.WriteStartElement("locals");
foreach (MDbgValue v in f.Function.GetActiveLocalVars(f))
{
DumpValue(v, xw);
}
}
catch
{
}
finally
{
xw.WriteEndElement(); // locals
}
// Write arguments
try
{
xw.WriteStartElement("arguments");
foreach (MDbgValue v in f.Function.GetArguments(f))
{
DumpValue(v, xw);
}
}
catch
{
}
finally
{
xw.WriteEndElement(); // arguments
}
}
}
catch
{
// Swallow all errors and keep trucking.
}
finally
{
xw.WriteEndElement(); // frame
}
}
// dump the Value to the xml stream.
static void DumpValue(MDbgValue v, XmlWriter xw)
{
DumpValueWorker(v, 2, xw);
}
// Helper to dump values.
static void DumpValueWorker(MDbgValue v, int depth, XmlWriter xw)
{
try
{
xw.WriteStartElement("value");
xw.WriteAttributeString("name", v.Name);
xw.WriteAttributeString("type", v.TypeName);
{
bool printSummary = true;
if (depth > 1)
{
// Dump sub items.
if (v.IsComplexType)
{
xw.WriteStartElement("fields");
MDbgValue[] fields = v.GetFields();
if (fields != null)
{
foreach (MDbgValue v2 in fields)
{
DumpValueWorker(v2, depth - 1, xw);
}
}
xw.WriteEndElement(); // "fields"
printSummary = false;
}
}
// If we haven't printed anything else, then print a summary
if (printSummary)
{
int expandDepth = 0;
bool fAllowFuncEval = false;
string val = v.GetStringValue(expandDepth, fAllowFuncEval);
if (val != "'\0'") // special case where WriteString breaks down on writing out '\0'.
{
xw.WriteString(val);
}
else
{
xw.WriteString("\\0");
}
}
}
}
finally
{
xw.WriteEndElement(); // Value
}
}
#endregion Dump to XML
#region Plumbing
// Once you first attach to a process, you need to drain a bunch of fake startup events
// for thread-create, module-load, etc.
static void DrainAttachEvents(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;
}
#endregion Plumbing
}
}