Friday, November 10, 2017

Debugging a Managed Memory Leak with WinDbg

WinDbg is an incredibly powerful debugger for Windows, that is chronically underused. The debugger has a steep learning curve, especially for people (like me) that mostly live in the wonderful world of the debugging tools integrated into Visual Studio. Unfortunately, sometimes you need to debug a leak in a production environment, or you have a managed-native interaction that you need to debug, and you can't install Visual Studio. WinDbg to the rescue.

If you haven't already, you will need to download and install the Debugging Tools For Windows. (You only need to install the tools on one computer. You can copy windbg.exe to your flash drive or a network share. For this exploration I'll be building my project in x64, and using the x64 version of the debugging tools.)

We need a simple program to test, so here's a quick one:

static void Main(string[] args)
{
    Test();
    Console.WriteLine("Attach the debugger. Press key after resuming");
    Console.ReadKey();
}

static List<string> guidList;
static void Test()
{
    guidList = new List<string>();
    for (int i = 0; i < 1000; i++)
    {
        guidList.Add(Guid.NewGuid().ToString());
    }
}

Build the executable, launch it, then fire up windbg. From the file menu select "Attach To Process" and find your running executable. (It may help to change the sort to "By Executable" which orders the processes alphabetically by name.) Click "OK" to attach the debugger. You are now presented with the command window of the debugger. A quick orientation may be in order:


The main area of the window is the debugger output. This is where the results of the commands you run will be output. The textbox at the bottom of the window is where you enter commands.

The first step is to load the SOS extension for the debugger, so it knows how to look at managed code. To do this we're going to use the .loadby command. (Note the period before) loadby loads a debugger extension by looking next to a dll that's already loaded into the target process. In this case we want to load the SOS module for the running version of the .NET runtime, which is located on the computer next to clr.dll. That means that the command is
.loadby sos clr
(If you are trying to debug an x86 or AnyCPU build using the x64 debugger, you will get an error here. You have to use the same architecture for both the debugger and the process.)

After you type the command and press enter you should see the command echoed in the debugger output. How do you know it worked? Well first there was no error. Second, you can use the .chain command to see what debugger extensions are loaded. At the top of the list, you should see SOS.

The next step is to get information about the managed heap. To do this, we will use the !DumpHeap command, and specify that we want statistical information. That command is:

!dumpheap -stat

Note: Debugger extension commands are always prefaced with an exclamation point ('!') and debugger commands are not case sensitive.

The data returned is in 4 columns:
  1. MT or Method Table. This is the memory address of the information about the object's structure.
  2. Count. The number of objects of that type on the managed heap.
  3. Total Size. The total size of the objects in bytes.
  4. Class Name. The name of the type, like System.String. Generic types have a compiler generated name, but you should be able to figure them out.
Looking at the returned table for our process, we can see that most of the objects have a relative small count, except for the row with System.String. On my system there are 1167 strings, taking up 105KB of memory. (Remember, our program 'leaks' strings.)  Let's dig in a little further and look at the objects of that type. The command is, helpfully:

!dumpheap -type System.String
This command will produce a lot of output. The first section is the individual objects, with three columns: Object Memory Address, MT, and Size. The second section shows some statistics about the objects in the list.  Scrolling through the first list, we can see a bunch of objects that are all 98 bytes. Let's look at one of them using the !DumpObj command. 
!dumpobj [address]
Replace [address] with the address from the first column. (You can omit leading zeroes.) The output will give you information about the object. Woohoo! We can see that our string is a GUID and now we know what is leaking.

Ok, but what if you want to look at more than one object. That's a lot of typing. Debugger markup language to the rescue. With the command:
.prefer_dml 1
you can turn on the DML version of the commands, and you will see links in the output that you can click, that enter the appropriate command for you.

As a final parting note, the SOS module has a lot more commands than the two we looked at today. There is a lot of good help built in. Just use the !help command to get a list of commands, and "!help [command]" to get more information about a specific command.

That's it for now. This was a very brief introduction that barely scratches the surface of this powerful tool. If this article was helpful, and you'd like to see more like it, feel free to leave a comment below.


No comments:

Post a Comment

All comments are moderated. I have a life, so it may take some time to show up. I reserve the right to delete any comment for any reason or no reason at all. Be nice. Racist, homophobic, transphobic, misogynist, or rude comments will get you banned.

Programmer vs Software Engineer: The Interview

A common question presented in interviews for developer positions goes something like this: Given an array of numbers, write a function th...