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


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:

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


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.

When WinDbg opens, it will stop on the "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.

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.

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


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

### 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.

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

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

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?

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

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