/windbg-ext-template

A template for creating managed WinDbg extensions

Primary LanguageC++MIT LicenseMIT

Managed WinDbg extension template

This repository contains code for a simple WinDbg extension. It implements two extension methods (hello_world, hello_world2) to prove that the idea works.

There are two versions available:

The former is more lightweight and runs on the latest versions of .NET. The latter requires a specific .NET version (matching the NuGet package) and has more dependencies. However, the exported PInvoke bindings are more polished. The choice is yours :)

How it works

The ext.cpp file contains the C++ code to load .NET runtime into the WinDbg process. The code is based on an excellent sample from the dotnet samples repository. Each extension method that WinDbg accepts must have the following signature:

HRESULT ext_method(IDebugClient* client, LPCSTR args)

IDebugClient is one of the many COM interfaces we use to communicate with the debug engine (dbgeng). In the past, accessing those interfaces from the managed world was complicated. Some of them were available in the clrmd project, but in a hard-to-consume form. Thanks to the NuGet packages mentioned before, we can now access them as any other COM objects. The Runtime-callable wrappers perform all the necessary actions to wrap and query the native COM objects. For example, to print something on the WinDbg output, we may cast the IDebugClient instance to IDebugControl7 and call the OutputWide method:

public static HRESULT HelloWorld(IDebugClient client, string args) {
        var ctrl = (IDebugControl7)client;
        ctrl.OutputWide(DEBUG_OUTPUT.NORMAL, "Hello world!\n");
}

From now, our options are endless. Check the dbgeng help to learn more.

Building

Make sure you have C++ tools and Windows SDK installed (you may need to update paths in the .vscode\c_cpp_properties.json if you're using Visual Studio Code). Then call: dotnet build.

Although the managed part could be compiled for any CPU, the native bootstrapper is either 64 or 32-bit. To explicitly set the bitness, specify the runtime identifier in the build command, for example, dotnet build --runtime=win-x86 --no-self-contained to build a 32-bit extension.

As a result of the build, you should find dbgxext.dll and DbgXExt.Lib.dll libraries in the output folder, among many other dependencies. The next step is to load dbgext.dll into WinDbg and execute the extension commands:

0:001> .load c:\Users\me\repos\windbg-ext-template\bin\Debug\net6.0-windows10.0.17763\win-x64\dbgxext.dll
0:001> !hello_world
Hello world!
0:001> !hello_world2
Hello world again!

Customizing the template for your own purpose

First, naming :) To change the manage library name, rename the csproj file. Then, you will need to accordingly update the constant strings in the ext.cpp file:

const wchar_t* dotnet_lib_name = L"DbgXExt.Lib.dll";
const wchar_t* dotnet_lib_config = L"DbgXExt.Lib.runtimeconfig.json";
const wchar_t* dotnet_type = L"DbgXExt.Lib.Ext, DbgXExt.Lib";
const wchar_t* dotnet_delegate_type = L"DbgXExt.Lib.Ext+ExtEntry, DbgXExt.Lib";

To change the name of the native bootstrapper, modify it in the csproj file:

<PropertyGroup>
  <NativeOutputName>dbgxext</NativeOutputName>
</PropertyGroup>

Finally, adding a new extension method goes in three steps:

  1. Add a new method to the managed Ext class, for example:
public sealed class Ext
{
    ...

    public static HRESULT MyAwesomeExtensionMethod(IDebugClient client, string args) {
        ...
    }
}
  1. Add a new method to the native ext.cpp file, for example:
extern "C" HRESULT CALLBACK my_awesome_extension_method(IDebugClient * dbgclient, PCSTR args) {
    static managed_extension_method fn{};
    static std::once_flag called{};
    std::call_once(called, []() {
        const std::wstring dotnetlib_path{ basedir / dotnet_lib_name };
        if (auto hr = loading_fn(dotnetlib_path.c_str(), dotnet_type, L"MyAwesomeExtensionMethod",
            dotnet_delegate_type, nullptr, reinterpret_cast<void**>(&fn)); FAILED(hr)) {
            fn = nullptr;
        }
        });

    return fn ? fn(dbgclient, args) : E_FAIL;
}
  1. Export the native method by adding it to the ext.def file, for example:
EXPORTS
    DebugExtensionNotify
    DebugExtensionInitialize
    DebugExtensionUninitialize

    my_awesome_extension_method

Then, after loading the bootstrapper dll, launch your method with the !my_awesome_extension_method command.

Things to improve

Adding a new method is a copy-paste exercise. I feel that it could be done better with some code-generation technique :)

Alternative approches

If you don't want to mess with C++ code, check the CLRMDExports project by Kevin Gosse. It is based on the clrmd package and could be a better option if you're implementing an extension to debug managed processes. Kevin describe this approach in an article on Medium.