WinDBG Extension written completely in C#

A lot of good and competent people shy away from writing their own WinDbg extension because of the difficultyto prepare a native C++ DLL using the right APIs to interact with WinDbg. So this post is to get you started to writing your own extension using a proof-of-concept extension that can analyze manage dumps (as well as native) taking care of the unnecessarily complex initialization and letting you go directly to the nitty-gritty part of writing the extension.

NetExt was originally written completely in C++ and then later adapted to be a C++, C# hybrid to leverage ClrMD (NetExt is prior to ClrMD). Since there were so many things already implemented in C++, the complete port was not practical (C++ also has its advantages after all). So let’s go in parts.

Structure of an extension

A debug extension must export this function that will be called to initialize the extension:

 HRESULT DebugExtensionInitialize(
  _Out_ PULONG Version,
  _Out_ PULONG Flags
);

 

The debug extension may export these functions:

 

 void CALLBACK DebugExtensionUninitialize(void); // Release any native resource you instantiated

 

 void CALLBACK DebugExtensionNotify(
  _In_ ULONG   Notify,
  _In_ ULONG64 Argument
); // It will be called when the state of the extension changes like when the debug session is active
 HRESULT CALLBACK KnownStructOutput(
  _In_    ULONG   Flag,
  _In_    ULONG64 Address,
  _In_    PSTR    StructName,
  _Out_   PSTR    Buffer,
  _Inout_ PULONG  BufferSize
); // To tell if a type structure can be formatted and printed by the extension (not used here)

 

The functions that can be called via ! are defined using this signature:

 typedef HRESULT ( CALLBACK *PDEBUG_EXTENSION_CALL)(
  _In_     PDEBUG_CLIENT Client,
  _In_opt_ PCSTR         Args
);

 

Some other challenges

This is accomplished via the call to dbgeng.dll function below. This will get the IDebugClient interface related to that session:

 HRESULT DebugCreate(
  _In_  REFIID InterfaceId,
  _Out_ PVOID  *Interface
);

 

From that you can get the IClientControl object that will let you write to the debugger via this function:

 HRESULT IDebugControl::ControlledOutput(
  [in] ULONG OutputControl,
  [in] ULONG Mask,
  [in] PCSTR Format,            ...
);

Passing the right mask like DEBUG_OUTCTL_DML can let you write hyperlinked output which we use in the extension.

Getting the pointers to WdbgExts API

Depending of the bitness of your target you will call either of these versions of the function:

 

HRESULT GetWindbgExtensionApis32(

[in, out] PWINDBG_EXTENSION_APIS64 Api

);

 

HRESULT GetWindbgExtensionApis64( [in, out] PWINDBG_EXTENSION_APIS64 Api );

 

 

In the structure it includes Ioctl which we will use to acquire the DLL used by WinDBG when you run .cordll –l without using a lot of tricks to load the right symbols

 ULONG Ioctl(  USHORT IoctlType,  PVOID  lpvData,  ULONG  cbSizeOfContext
);

 

Though it is not mandatory, extensions normally include the help function to be called via !help and provides help on how to use the extension.

As we need to export the function as unmanaged code I used the DllExport NuGet,

 

NOTE

Though I believe I resolved the issue, it may be necessary that you copy the extension to the very same folder where WinDBG is located.

 

Download the project here

SourceExt

 

The extension code

 

 /*=========================================================================================================
   Copyright (c) 2016 Rodney Viana
   https://netext.codeplex.com/
 
   Distributed under GNU General Public License version 2 (GPLv2) (https://www.gnu.org/licenses/gpl-2.0.html)
 ============================================================================================================*/
 
 
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using RGiesecke.DllExport;
 using System.Runtime.InteropServices;
 using Microsoft.Diagnostics.Runtime.Interop;
 using Microsoft.Diagnostics.Runtime;
 using System.Security.Policy;
 
 namespace SourceExt
 {
 
     public enum HRESULT : uint
     {
         S_OK = 0,
         E_ABORT = 4,
         E_ACCESSDENIED = 0x80070005,
         E_FAIL = 0x80004005,
         E_HANDLE = 0x80070006,
         E_INVALIDARG = 0x80070057,
         E_NOINTERFACE = 0x80004002,
         E_NOTIMPL = 0x80004001,
         E_OUTOFMEMORY = 0x8007000E,
         E_POINTER = 0x80004003,
         E_UNEXPECTED = 0x8000FFFF
 
     }
     public class SourceFixExt
     {
         [DllImport("dbgeng.dll")]
         internal static extern uint DebugCreate(ref Guid InterfaceId, [MarshalAs(UnmanagedType.IUnknown)] out object Interface);
 
 
 
         internal static HRESULT LastHR;
 
 
         internal delegate uint Ioctl(IG IoctlType, ref WDBGEXTS_CLR_DATA_INTERFACE lpvData, int cbSizeOfContext);
 
         internal static IDebugClient5 client = null;
         internal static IDebugControl6 control = null;
         internal static DataTarget target = null;
         internal static ClrRuntime runtime = null;
 
 
         private static HRESULT Int2HResult(int Result)
         {
             // Convert to Uint
             uint value = BitConverter.ToUInt32(BitConverter.GetBytes(Result), 0);
 
             return Int2HResult(value);
         }
 
         private static HRESULT Int2HResult(uint Result)
         {
             HRESULT hr = HRESULT.E_UNEXPECTED;
             try
             {
                 hr = (HRESULT)Result;
 
             }
             catch
             {
 
             }
             return hr;
         }
         private static IDebugClient CreateIDebugClient()
         {
 
             Guid guid = new Guid("27fe5639-8407-4f47-8364-ee118fb08ac8");
             object obj;
             var hr = DebugCreate(ref guid, out obj);
             if (hr < 0)
             {
                 LastHR = Int2HResult(hr);
                 WriteLine("SourceFix: Unable to acquire client interface");
                 return null;
             }
             IDebugClient client = (IDebugClient5)obj;
             return client;
         }
 
         internal static void INIT_API()
         {
             LastHR = HRESULT.S_OK;
             if (client == null)
             {
                 try
                 {
 
                     client = (IDebugClient5)CreateIDebugClient();
                     control = (IDebugControl6)client;
                 }
                 catch
                 {
                     LastHR = HRESULT.E_UNEXPECTED;
                 }
 
             }
         }
 
         internal static void INIT_CLRAPI()
         {
             INIT_API();
             if (LastHR != HRESULT.S_OK)
             {
                 runtime = null;
                 target = null;
                 return;
             }
             if (runtime != null)
             {
                 return;
             }
             Ioctl ioctl = null;
             WINDBG_EXTENSION_APIS apis = new WINDBG_EXTENSION_APIS();
             apis.nSize = (uint)Marshal.SizeOf(apis);
             Guid IXCLRData = new Guid("5c552ab6-fc09-4cb3-8e36-22fa03c798b7");
             if(Marshal.SizeOf(IntPtr.Zero) == 8)
             {
                 LastHR = Int2HResult(control.GetWindbgExtensionApis64(ref apis));
             } else
             {
                 LastHR = Int2HResult(control.GetWindbgExtensionApis32(ref apis));
             }
 
 
             ioctl = (Ioctl)Marshal.GetDelegateForFunctionPointer((IntPtr)apis.lpIoctlRoutine, typeof(Ioctl));
             WDBGEXTS_CLR_DATA_INTERFACE clr = new WDBGEXTS_CLR_DATA_INTERFACE();
             unsafe
             {
                 clr.Iid = &IXCLRData;
             }
 
             if (ioctl(IG.GET_CLR_DATA_INTERFACE, ref clr, Marshal.SizeOf(clr)) == 0)
             {
                 WriteLine("ERROR: Unable to load .NET interface");
                 WriteLine("Run the command below to check if .NET Interface can be loaded:");
                 WriteLine(".cordll -u -ve -l");
                 client = null;
                 WriteLine("SourceFix: Unable to acquire .NET interface");
                 LastHR = HRESULT.E_NOINTERFACE;
                 target = null;
                 runtime = null;
 
             }
             else
             {
                 target = DataTarget.CreateFromDebuggerInterface(client);
 
 
                 runtime = target.ClrVersions.Single().CreateRuntime(clr.Interface);
             }
 
 
         }
 
 #region Mandatory
 
         internal static AppDomain currDomain = null;
 
         [DllExport]
         public static HRESULT DebugExtensionInitialize(ref uint Version, ref uint Flags)
         {
             uint Major = 1;
             uint Minor = 0;
             Version = (Major << 16) + Minor;
             Flags = 0;
 
             // Everything below is to enable the extension to be loaded from another
             //  folder that is not the same of the Debugger
 
             // Set up the AppDomainSetup
             AppDomainSetup setup = new AppDomainSetup();
             string assembly = System.Reflection.Assembly.GetExecutingAssembly().CodeBase;
             setup.ApplicationBase = assembly.Substring(0, assembly.LastIndexOf('\\') + 1);
 
             setup.ConfigurationFile = assembly + ".config";
 
 
             // Set up the Evidence
             Evidence baseEvidence = AppDomain.CurrentDomain.Evidence;
             Evidence evidence = new Evidence(baseEvidence);
 
 
             currDomain = AppDomain.CreateDomain("SourceExt", AppDomain.CurrentDomain.Evidence,
               setup );
             currDomain.UnhandledException += CurrDomain_UnhandledException;
 
             return HRESULT.S_OK;
         }
 
         private static void CurrDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
         {
             INIT_API();
             WriteLine("SourceExt: An unhandled exception happened on the extension");
             WriteLine("  This error is related to the extension itself not the target.");
             WriteLine("  The information on the exception is below:\n");
 
             Exception ex = e.ExceptionObject as Exception;
             while (ex != null)
             {
                 WriteLine("{0} - {1} at", ex.GetType().ToString(), ex.Message);
                 WriteLine("{0}", ex.StackTrace);
                 ex = ex.InnerException;
                 if (ex != null)
                 {
                     WriteLine("\n----- Inner Exception -----------");
                 }
             }
         }
 
         private static bool showedHello = false;
 
         [DllExport]
         public static void DebugExtensionNotify(uint Notify, ulong Argument)
         {
             if (Notify == 2) // I can write now
             {
                 if (!showedHello) // Just once
                 {
                     INIT_API();
                     WriteDmlLine("<b>SourceExt</b> 1.0");
                     WriteDmlLine("For help, type <link cmd=\"!help\">!help</link>");
                     showedHello = true;
                 }
             }
         }
 
         [DllExport]
         public static HRESULT DebugExtensionUninitialize()
         {
             client = null;
             target = null;
             control = null;
             runtime = null;
 
             if (currDomain != null)
             {
                 AppDomain.Unload(currDomain);
             }
 
             return HRESULT.S_OK;
         }
 
 #endregion
 
 #region Commands
 
 
         [DllExport]
         public static HRESULT help(IntPtr client, [MarshalAs(UnmanagedType.LPStr)] string Args)
         {
             INIT_API();
 
             if (String.IsNullOrWhiteSpace(Args))
             {
                 WriteDmlLine("<link cmd=\"!help heapstat\">heapstat</link> - Displays heap statistics");
                 WriteDmlLine("<link cmd=\"!help stack\">stack</link> - Displays stack trace");
             }
             else
             {
                 switch (Args.Trim().ToLower())
                 {
                     case "heapstat":
                         WriteLine("Display heaps statics ordered by memory usage\n");
                         WriteLine("Syntax: heapstat");
                         break;
                     case "stack":
                         WriteLine("Dump managed stack trace from all managed threads\n");
                         WriteLine("Syntax: stack");
                         break;
                     default:
                         WriteLine("Command '{0}' not found", Args);
                         break;
 
                 }
             }
 
             return HRESULT.S_OK;
         }
 
         [DllExport]
         public static HRESULT heapstat(IntPtr client, [MarshalAs(UnmanagedType.LPStr)] string Args)
         {
             INIT_CLRAPI();
             if (LastHR != HRESULT.S_OK)
                 return LastHR;
 
 
 
             // Walk the entire heap and build heap statistics in "stats".
             Dictionary<ClrType, Entry> stats = new Dictionary<ClrType, Entry>();
 
             // This is the way to walk every object on the heap:  Get the ClrHeap instance
             // from the runtime.  Walk every segment in heap.Segments, and use
             // ClrSegment.FirstObject and ClrSegment.NextObject to iterate through
             // objects on that segment.
             ClrHeap heap = runtime.GetHeap();
             foreach (ClrSegment seg in heap.Segments)
             {
                 for (ulong obj = seg.FirstObject; obj != 0; obj = seg.NextObject(obj))
                 {
                     // This gets the type of the object.
                     ClrType type = heap.GetObjectType(obj);
                     ulong size = type.GetSize(obj);
 
 
 
                     // Add an entry to the dictionary, if one doesn't already exist.
                     Entry entry = null;
                     if (!stats.TryGetValue(type, out entry))
                     {
                         entry = new Entry();
                         entry.Name = type.Name;
                         stats[type] = entry;
                     }
 
                     // Update the statistics for this object.
                     entry.Count++;
                     entry.Size += type.GetSize(obj);
                 }
             }
 
 
 
             // We'll actually let linq do the heavy lifting.
             var sortedStats = from entry in stats.Values
                               orderby entry.Size
                               select entry;
 
             WriteLine("{0,12} {1,12} {2}", "Size", "Count", "Type");
             foreach (var entry in sortedStats)
                 WriteLine("{0,12:n0} {1,12:n0} {2}", entry.Size, entry.Count, entry.Name);
             return 0;
 
         }
 
         [DllExport]
         public static HRESULT stack(IntPtr client, [MarshalAs(UnmanagedType.LPStr)] string Args)
         {
             INIT_CLRAPI();
             if (LastHR != HRESULT.S_OK)
                 return LastHR;
 
             foreach (ClrThread thread in runtime.Threads)
             {
                 // The ClrRuntime.Threads will also report threads which have recently died, but their 
                 // underlying datastructures have not yet been cleaned up.  This can potentially be 
                 // useful in debugging (!threads displays this information with XXX displayed for their 
                 // OS thread id).  You cannot walk the stack of these threads though, so we skip them 
                 // here. 
                 if (!thread.IsAlive)
                     continue;
 
 
                 WriteLine("Thread {0:%p}:", thread.OSThreadId);
                 WriteLine("Stack: {0:%p} - {1:%p}", thread.StackBase, thread.StackLimit);
 
 
                 // Each thread tracks a "last thrown exception".  This is the exception object which 
                 // !threads prints.  If that exception object is present, we will display some basic 
                 // exception data here.  Note that you can get the stack trace of the exception with 
                 // ClrHeapException.StackTrace (we don't do that here). 
                 ClrException exception = thread.CurrentException;
                 if (exception != null)
                     WriteLine("Exception: {0:%p} ({1}), HRESULT={2:%p}", exception.Address, exception.Type.Name, exception.HResult);
 
 
                 // Walk the stack of the thread and print output similar to !ClrStack. 
                 WriteLine("");
                 WriteLine("Managed Callstack:");
                 foreach (ClrStackFrame frame in thread.StackTrace)
                 {
                     // Note that CLRStackFrame currently only has three pieces of data: stack pointer, 
                     // instruction pointer, and frame name (which comes from ToString).  Future 
                     // versions of this API will allow you to get the type/function/module of the 
                     // method (instead of just the name).  This is not yet implemented. 
                     WriteLine("{0:%p} {1:%p} {2}", frame.StackPointer, frame.InstructionPointer, frame.DisplayString);
                 }
 
                 WriteLine("");
 
             }
 
             return HRESULT.S_OK;
         }
 
 #endregion
 
 #region Helpers
 
 
 
         private static string pFormat = String.Format(":x{0}", Marshal.SizeOf(IntPtr.Zero) * 2);
         public static string pointerFormat(string Message)
         {
 
             return Message.Replace(":%p", pFormat);
 
         }
 
         [DllExport(CallingConvention = CallingConvention.Cdecl)]
         public static void Echo([MarshalAs(UnmanagedType.LPWStr)]string Message)
         {
             Out(Message);
         }
 
         public static void Write(string Message, params object[] Params)
         {
             if (Params == null)
                 Out(Message);
             else
                 Out(String.Format(pointerFormat(Message), Params));
         }
 
         public static void WriteLine(string Message, params object[] Params)
         {
             if (Params == null)
                 Out(Message);
             else
                 Out(String.Format(pointerFormat(Message), Params));
             Out("\n");
         }
 
         public static void WriteDml(string Message, params object[] Params)
         {
             if (Params == null)
                 OutDml(Message);
             else
                 OutDml(String.Format(pointerFormat(Message), Params));
         }
 
         public static void WriteDmlLine(string Message, params object[] Params)
         {
             if (Params == null)
                 OutDml(Message);
             else
                 OutDml(String.Format(pointerFormat(Message), Params));
             Out("\n");
         }
         public static void Out(string Message)
         {
             control.ControlledOutput(DEBUG_OUTCTL.ALL_CLIENTS, DEBUG_OUTPUT.NORMAL, Message);
         }
 
         public static void OutDml(string Message)
         {
             control.ControlledOutput(DEBUG_OUTCTL.ALL_CLIENTS | DEBUG_OUTCTL.DML, DEBUG_OUTPUT.NORMAL, Message);
 
         }
 
 #endregion
     }
 
 #region Miscelanious
     class Entry
     {
         public string Name;
         public int Count;
         public ulong Size;
     }
 
 #endregion
 }

 

Feel free to change, add and remove features. The code is licensed as GPL.