.NET Core and WinDbg

I just wasted a few minutes of my life trying to figure out how to attach WinDbg to a .NET Core console app... so I thought I'd share, and perhaps save someone else the time. If you've ever attached to a .NET Framework console app, it's pretty similar, with just a couple of gotchas.

If you're already familiar with creating and running console apps in .NET Core, skip down to the Launch with WinDbg Attached section. If you're already familiar with debugging .NET Framework apps with WinDbg, you can probably skip straight to the recap.

Create a .NET Core Console App

First, let's create a new .NET Core console app using the dotnet command-line interface:

> dotnet new console
> dotnet restore
> dotnet build -c release

dotnet new console

The first notable problem is that .NET Core build doesn't generate an exe file. In the bin\release\netcoreapp2.0 folder, all we have are these:

bin folder

Note: I'm using a preview version of the dotnet CLI, so I see output in netcoreapp2.0, but the version number at the end may vary for you.

Our "console application" is in that Sample.dll, but we need something to run it. There are a few different ways to do this, but the easiest way is to use the dotnet CLI itself.

> dotnet path\to\Sample.dll

dotnet Sample.dll

Great, now that we have a runnable console app, let's add a method for us to debug or disasseble with WinDbg. I've written a dummy method called "SlowMultiply" which performs multiplication the slow-route (by adding). Feel free to write whatever kind of method you'd like.

Here's the whole program now:

using System;  
using System.Runtime.CompilerServices;

namespace Sample  
{
    static class Program
    {
        static void Main(string[] args)
        {
            var result = SlowMultiply(-7, 5);
            Console.WriteLine(result);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        static int SlowMultiply(int x, int y)
        {
            if (y < 0)
            {
                x *= -1;
                y *= -1;
            }

            var result = 0;
            for (var i = 0; i < y; i++)
            {
                result += x;
            }

            return result;
        }
    }
}

Note: I'm using the [MethodImpl(MethodImplOptions.NoInlining)] attribute to make sure the JIT doesn't decide to inline this method. We won't be able to disassemble the method on its own if it gets inlined.

Now if we build and run the program, we should get an output of -35.

Launch with WinDbg Attached

Make sure you have WinDbg installed.

I find attaching to an already running console app to be a bit of a pain, so I generally prefer to launch the executable directly from WinDbg. But remember, with .NET Core, there isn't usually an executable to launch. Earlier we launched our dll via the dotnet CLI. We'll take the same approach for launching from WinDbg.

You could use the "Open Executable" menu in WinDbg, but the much easier option is to add the directory containing windbg.exe to your path. It's typically a location such as: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64. Make sure to add the x64 directory, not x86. You'll need to restart cmd for any changes to take effect.

Now you can launch your console app with WinDbg already attached by simply running:

> windbg dotnet path\to\Sample.dll

Set a Breakpoint

Of course, attaching a debugger isn't very useful if we don't set any breakpoints. There are ways to do that in WinDbg, but I prefer the simplicity of specifying a breakpoint in code.

In this example, we'd like to take a look at the disassembly for SlowMultiply, so I'm going to set a breakpoint right after it's called. I'm placing it after the call so that the method will be JIT'ed by the time we hit the breakpoint.

static void Main(string[] args)  
{
    var result = SlowMultiply(-7, 5);
    System.Diagnostics.Debugger.Break(); // breakpoint
    Console.WriteLine(result);
}

Make sure to build again after making this change (dotnet build -c release).

Alright, let's try it out.

windbg dotnet Sample.dll

When WinDbg opens, it will stop on the "first chance" breakpoint.

first chance breakpoint

The CLR isn't even loaded yet, so we can't really do much at this point. Type "g" into the WinDbg console and hit enter to go to the next breakpoint.

user breakpoint

Now we're at the breakpoint that we set! We could open the disassembly window and start debugging immediately, but there's a few things we should do first that will make our lives easier.

Load SOS

SOS.dll acts as an extension to WinDbg which provides information about managed code. If you're using the .NET Framework, the easiest way to load sos.dll is via the command .loadby sos clr. That command says "load sos.dll from the same directory where clr.dll was loaded from."

However, if you run that command on .NET Core, you'll get: Unable to find module 'clr'. This is because .NET Core doesn't use "clr.dll", it loads "coreclr.dll". So, instead, load SOS like this:

.loadby sos coreclr

.loadby sos coreclr

Other than the absence of an error message, there won't be any indication that it loaded properly.

SOS Loading Success

View Disassembly of a Method

With SOS loaded, now we can finally disassemble our "SlowMultiply" method. For that, let's use !name2ee. It takes two arguments: the dll (or exe) name, followed by the fully-qualified method name.

!name2ee Sample.dll Sample.Program.SlowMultiply

name2ee outputs some basic information about the method. If the method hadn't been JIT'ed yed, it will provide a link for setting a breakpoint the first time it's called. In our case, the method already exists, so we get the address of its code.

If name2ee doesn't produce any output, make sure you typed everything correctly and fully qualified the method name, including the namespace and class name.

name2ee

Clicking the blue link following "JITTED Code Address" will show you the disassembly:

disassembly

If the disassembly includes line numbers (like shown above), congrats, your symbols were loaded successfully! But... what if you're not so lucky?

Load Symbols

Did you get something that said:

ERROR: Module load completed but symbols could not be loaded for c:\code\blog\CoreWinDbg\Sample\bin\Release\netcoreapp1.0\Sample.dll  

And the output didn't have any source file or line number information?

no line numbers

Bummer...

I've noticed this happens with .NET Core console apps I create with Visual Studio. Verbose symbol loading in WinDbg (via !sym noisy) prints largely misleading error messages in this case - something about the .pdb file not existing or not being able to copy it to cache. The .pdb file exists, but it seems to be in a format WinDbg doesn't understand.

Luckily, the solution is quite simple. Add these two lines to your .csproj file:

<DebugSymbols>true</DebugSymbols>  
<DebugType>pdbonly</DebugType>  

For example:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.0</TargetFramework>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>pdbonly</DebugType>
  </PropertyGroup>

</Project>  

Rebuild the project, and now you'll have symbols loaded the next time you try to view the disassembly.

Recap

  1. Add windbg.exe (the x64 version) to your path
  2. Set breakpoint(s) using System.Diagnostics.Debugger.Break()
  3. Launch with WinDbg attached via windbg dotnet path\to\Your.dll
  4. Enter g to go to your first breakpoint
  5. Load SOS via .loadby sos coreclr
  6. If you don't get symbols for your DLL, add <DebugSymbols>true</DebugSymbols> and <DebugType>pdbonly</DebugType> to your .csproj file and rebuild

If you're new to WinDbg, keep in mind there are many ways to attach WinDbg, many ways to set breakpoints, and there's a whole lot more you can do with WinDbg than just look at method disassembly. Hopefully this post got you in the door of .NET Core debugging; internet search engines should take you the rest of the way....