Before I came to work at Microsoft I worked as a professional developer on the Microsoft platform and I used to work with the Visual Studio (6 latterly) debugger which I thought was a pretty cool debugger.
When I joined Microsoft I found that a lot of people worked with WinDbg as their primary debugger and, for a while, I just didn't get it as it seemed like an unnecessarily grungy tool for achieving things that Visual Studio could already do.
However, over a period of time I came to see the light and I'm now of the view that WinDbg is about the best debugger I've ever used (including those ones I used to work with on Unix platforms). WinDbg is just the most powerful debugger out there for user-mode debugging and I don’t do any other kind of debugging J
So, there is a lot of information out there already on WinDbg but I just wanted to try and highlight it here and provide a single-pager on it as it seems to be grossly overlooked as a debugger for managed code and, yet, it’s capable of a whole tonne of stuff that no single other debugger can do for you right now.
People are often put off WinDbg because of its interface and the vast array of commands so in this little series of postings I’ll try and demystify a little bit and show how you can debug a few common scenarios in managed code with it.
In this post, let’s just get to the point where we can use WinDbg to debug a regular process and do some work with it then in the next post(s) I’ll look at more stuff that you can do.
So, first things first, get hold of a copy of WinDbg from here and install it. If you want to follow along with this post then you can go and download it right now - the install takes about 2 minutes once you've got the 10MB download down from the site.
The first thing you'll realise is how lightweight an installation WinDbg is and that (sometimes) means that you can install it into places that you'd never install a copy of Visual Studio or the Visual Studio remote debug set up. Indeed, often if you raise a support call with Microsoft you'll get asked to install WinDbg and it's automated helper (the "autodumper") in order to get a crash dump of your application to send back to Microsoft.
Before we get into debugging something I need to have a quick word or two about Symbols and Commands.
The first issue that we need to address in order to progress is that of symbols. If you want to get decent stack traces and dumps of variable values and so on out of the debugger then you need symbols for the modules that you’re debugging whether those modules are yours or someone else’s and whether those modules are managed or unmanaged (although there’s quite a lot more that you can do with managed code without symbols).
Symbols are typically either private symbols (include variable information), retail symbols (function information but not variable information) or export symbols (generally not so useful for debugging purposes). If you’re debugging with “export” symbols it’s not usually enough to actually work out what’s going on.
For your own code you’re responsible for building the symbol files (usually .PDB program databases) by setting the right flags on the compiler or choosing the right configuration in a project (this is true for VC++, VB6 and all the .NET languages). This will give you full private symbols.
When the debugger (and lots of other tools) want to find symbols they typically check the contents of an environment variable named _NT_SYMBOL_PATH which is used (just like PATH – i.e. a list of folder separated by semi-colons) to find symbol files. You can set _NT_SYMBOL_PATH prior to running WinDbg or you can use the debugger’s own symbol path settings to find symbol files. These settings are controlled via the File->Symbol File Path menu option or using the .sympath command.
For other people’s code you need to get symbols from them. For Microsoft code you’re in luck because the symbols are available from a public symbol server on the internet. In order to make use of the symbol server you should set your symbol path to be something like;
SRV*C:/MyLocalSymbols*http://msdl.microsoft.com/download/symbols
What this says is that the debugger should first check a local cache named C:/MyLocalSymbols and if symbols are not found there then go out to the Microsoft symbol server for symbols and, if found, download them and cache them in that folder. Note that symbols are downloaded based upon name, version and checksum information from the modules being debugged so the debugger usually knows if the symbols match the code properly.
If you wanted to combine this with a local location for your own code then you’d use something like;
C:/MyCodesSymbols; SRV*C:/MyLocalSymbols*http://msdl.microsoft.com/download/symbols
so that the debugger would check your folder first and would then proceed to look in the cache and finally to the symbol server for symbols.
A core set of commands would be as below. I rarely venture beyond these as I’m not an advanced user of WinDbg by any means and this gets me by.
.hh - bring up the help file :-) The help file is fantastic so don't skip on it - everything's documented in there.
g - "GO!". That is, continue running.
bp (Address) - set a break point. Note that (address) can take many formats here including just a memory location but the most common format specifier here is to use syntax such as;
bp KERNEL32!CreateFileA
(remember that most Windows functions have an ASCII and a Unicode variant so we have KERNEL32!CreateFileA and KERNEL32!CreateFileW).
bl - list all breakpoints. Each breakpoint listed has a number in the list which you need for...
bc (number), be (number), bd (number) - these respectively clear, enable and disable breakpoints from the list.
kb, kp, kd etc. The commands beginning with "k" show the stack for the thread that the debugger is looking at. There are a few variants so check out the help for those. The simplest variant that I use is;
kb 200
(200 is the maximum depth of stack trace that I'm looking for)
dw, db, ds, etc. The commands beginning with "d" show the contents of memory. One of the most common is "dt" which shows you the contents of memory laid out according to the specification of a type as long as the debugger can see the definition of a type.
sxe, sxd, sxi, sxn. The commands beginning with "sx" set the behaviour for what the debugger should do when an exception occurs. A good example here would be;
sxe 0xc0000005
which is saying "I want to break into the debugger if there's an access violation".
lm - lists the modules loaded by the program and what kind of symbols are loaded for the modules. Note that export symbols are not really symbols at all but are really the debugger guessing which function you're in based upon the export table of the DLL. For COM servers in particular (which have only 4 exported functions or so) you'll often find yourself appearing in DllRegisterServer if you only have export symbols. Good examples here would be;
lm v (verbose mode)
lm v mUSER* (verbose mode, matc any modules that begin with USER*)
x - Examine symbols. This is very, very useful as it shows you the set of symbols loaded for particular modules. A good example here would be;
x USER32!* (show me all the symbols loaded from the USER32 module)
x USER32!Create* (show me all the symbols loaded from the USER32 module that begin with "Create")
.cls – clear the screen. You’ll be needing this one!
.reload – this causes the symbol information to be reloaded for a particular module. The most common form of this would be to do something like;
.reload /f user32.dll
Where the /f overrides the debugger’s naturally lazy mode of working whereby it wouldn’t actually do the reload right there and then. Normally, this is a useful parameter to use.
~ - this is the tilde character and it lists all the threads in the process and shows their status. This can be combined with wildcards in order to execute particular commands on all threads in the process. The single most common one is probably to combine it with a “stack” command to do something like;
~* kb 200
which will show you the stack frames for all the threads in the process.
~N s – this changes the debugger’s focus to another thread. For example;
~7 s
Will switch the debugger so that it’s focused on thread 7. Note that 7 here is the debugger’s thread number rather than the real thread ID and it comes from the list given by the ~ command.
Along with the core set of commands, WinDbg is capable of loading up debugger extensions to extend the core functionality of the debugger. These are “bang commands” in that they begin with an exclamation mark. You can manipulate the extensions that the debugger has loaded using the following commands;
.chain – this shows the extensions that the debugger has loaded
.load – this loads an extension DLL (e.g. .load SOS.DLL)
.unload – unloads an extension DLL
.unloadall
.setdll – this sets the “default” extension DLL.
A quick word about .setdll and how command processing works here. If you have an extension named UEXT.DLL which contains a command named help (and most extensions will have a help command) then you can run that command using;
!UEXT.help
And that works fine. If you find yourself using UEXT more than any other extension then you can use;
.setdll UEXT.DLL
To make UEXT.DLL the default extension and then you can just use;
!help
And that will now default to mean !UEXT.help because of the default extension setting.
So, let’s finish up this post with a quick debugging session and next time around I’ll talk about debugging managed code rather than just any old process.
0:001> lm
start end module name
01000000 01014000 notepad (no symbols)
4ce00000 4cf02000 COMCTL32 (export symbols) C:/WINDOWS/WinSxS/x86_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.2600.2149_x-ww_a84b1f06/COMCTL32.dll
5ad70000 5ada7000 uxtheme (export symbols) C:/WINDOWS/system32/uxtheme.dll
605d0000 605d9000 mslbui (export symbols) C:/WINDOWS/system32/mslbui.dll
61220000 61232000 MSH_ZWF (export symbols) C:/Program Files/Microsoft Hardware/Mouse/MSH_ZWF.dll
73000000 73026000 WINSPOOL (export symbols) C:/WINDOWS/system32/WINSPOOL.DRV
74720000 7476b000 MSCTF (export symbols) C:/WINDOWS/system32/MSCTF.dll
763b0000 763f9000 comdlg32 (export symbols) C:/WINDOWS/system32/comdlg32.dll
77120000 771ac000 OLEAUT32 (export symbols) C:/WINDOWS/system32/OLEAUT32.DLL
771b0000 779c4000 SHELL32 (export symbols) C:/WINDOWS/system32/SHELL32.dll
779d0000 77a46000 SHLWAPI (export symbols) C:/WINDOWS/system32/SHLWAPI.dll
77a50000 77b8d000 ole32 (export symbols) C:/WINDOWS/system32/ole32.dll
77c10000 77c68000 msvcrt (export symbols) C:/WINDOWS/system32/msvcrt.dll
77cc0000 77d5b000 ADVAPI32 (export symbols) C:/WINDOWS/system32/ADVAPI32.dll
77d60000 77df1000 USER32 (export symbols) C:/WINDOWS/system32/USER32.dll
77e00000 77e93000 RPCRT4 (export symbols) C:/WINDOWS/system32/RPCRT4.dll
77ea0000 77ee5000 GDI32 (export symbols) C:/WINDOWS/system32/GDI32.dll
7c800000 7c8f2000 kernel32 (export symbols) C:/WINDOWS/system32/kernel32.dll
7c900000 7c9ae000 ntdll (export symbols) C:/WINDOWS/system32/ntdll.dll
SRV*e:/LocalSymbolCache*http://msdl.microsoft.com/download/symbols
0:001> lm
start end module name
01000000 01014000 notepad (pdb symbols) e:/LocalSymbols/notepad.pdb/15800B8231AF4FDE85232D42B267D3E51/notepad.pdb
4ce00000 4cf02000 COMCTL32 (pdb symbols) e:/LocalSymbols/MicrosoftWindowsCommon-Controls-6.0.2600.2149-comctl32.pdb/4133208CAF9A4B93982A1F873FBC79261/MicrosoftWindowsCommon-Controls-6.0.2600.2149-comctl32.pdb
5ad70000 5ada7000 uxtheme (pdb symbols) e:/LocalSymbols/uxtheme.pdb/3A4D37B97BA34837AC589BDB15CC265D2/uxtheme.pdb
605d0000 605d9000 mslbui (pdb symbols) e:/LocalSymbols/MSLBUI.pdb/ED5CCEFBD83C4F929AAB9A1029C54DDD1/MSLBUI.pdb
61220000 61232000 MSH_ZWF (export symbols) C:/Program Files/Microsoft Hardware/Mouse/MSH_ZWF.dll
73000000 73026000 WINSPOOL (pdb symbols) e:/LocalSymbols/winspool.pdb/20EE1501116B4C4180ABE40B209DEC962/winspool.pdb
74720000 7476b000 MSCTF (pdb symbols) e:/LocalSymbols/msctf.pdb/F8F1C6E0E1CC4D1CB4936C12A6A6D4C92/msctf.pdb
763b0000 763f9000 comdlg32 (pdb symbols) e:/LocalSymbols/comdlg32.pdb/2BC57F8BE63A429FB4B439EA2854387A2/comdlg32.pdb
77120000 771ac000 OLEAUT32 (pdb symbols) e:/LocalSymbols/oleaut32.pdb/96D8A9A6CB704201A365F345B2A9F72D2/oleaut32.pdb
771b0000 779c4000 SHELL32 (pdb symbols) e:/LocalSymbols/shell32.pdb/FE0DC899DD78422AB29561FE8255E1C92/shell32.pdb
779d0000 77a46000 SHLWAPI (pdb symbols) e:/LocalSymbols/shlwapi.pdb/231AB6F0A19F4DBB9837440BCC167BEC2/shlwapi.pdb
77a50000 77b8d000 ole32 (pdb symbols) e:/LocalSymbols/ole32.pdb/656B303C367B4E679CA249E32CE572D92/ole32.pdb
77c10000 77c68000 msvcrt (pdb symbols) e:/LocalSymbols/msvcrt.pdb/8E17C9B9A55C4118A7D35025C5BE51871/msvcrt.pdb
77cc0000 77d5b000 ADVAPI32 (pdb symbols) e:/LocalSymbols/advapi32.pdb/57E2A801C610451AA66B7CD94F0910F72/advapi32.pdb
77d60000 77df1000 USER32 (pdb symbols) e:/LocalSymbols/user32.pdb/35948255FAC04CA99F5CF61959B45DC42/user32.pdb
77e00000 77e93000 RPCRT4 (pdb symbols) e:/LocalSymbols/rpcrt4.pdb/79C7047CB77B435DB01EB365C3AF17152/rpcrt4.pdb
77ea0000 77ee5000 GDI32 (pdb symbols) e:/LocalSymbols/gdi32.pdb/E7D5B033EFE5466E97DB048BE4C88FA62/gdi32.pdb
7c800000 7c8f2000 kernel32 (pdb symbols) e:/LocalSymbols/kernel32.pdb/5091FA1742544F2EA3D425642634F78E2/kernel32.pdb
7c900000 7c9ae000 ntdll (pdb symbols) e:/LocalSymbols/ntdll.pdb/7B2A2EF0E63F4848A8D309E84E47322C2/ntdll.pdb
0:000> kb
ChildEBP RetAddr Args to Child
0006be18 76671b2a 76671b74 00000020 00000003 kernel32!CreateFileW
0006be94 766724e2 000a6b40 0006bf4c 00000000 cscui!IsCSCEnabled+0x38
0006bea8 77a68b49 000a7084 77a51a60 0006bf44 cscui!DllGetClassObject+0x72
0006bec4 77a80f5e 000a7084 77a51a60 0006bf44 ole32!CClassCache::CDllPathEntry::DllGetClassObject+0x2d
0006bedc 77a80e9a 0006bef0 77a51a60 0006bf44 ole32!CClassCache::CDllFnPtrMoniker::BindToObjectNoSwitch+0x1f
0006bf08 77a81cc6 0006bf4c 00000000 0006c540 ole32!CClassCache::GetClassObject+0x38
0006bf84 77a806aa 77b76ca4 00000000 0006c540 ole32!CServerContextActivator::CreateInstance+0x106
0006bfc4 77a81e19 0006c540 00000000 0006ca8c ole32!ActivationPropertiesIn::DelegateCreateInstance+0xf7
0006c018 77a81d90 77b76ca8 00000000 0006c540 ole32!CApartmentActivator::CreateInstance+0x110
0006c038 77a8101e 77b76ca8 00000001 00000000 ole32!CProcessActivator::CCICallback+0x6d
0006c058 77a80fd5 77b76ca0 0006c39c 00000000 ole32!CProcessActivator::AttemptActivation+0x2c
0006c090 77a81e7a 77b76ca0 0006c39c 00000000 ole32!CProcessActivator::ActivateByContext+0x42
0006c0b8 77a806aa 77b76ca0 00000000 0006c540 ole32!CProcessActivator::CreateInstance+0x49
0006c0f8 77a81bc4 0006c540 00000000 0006ca8c ole32!ActivationPropertiesIn::DelegateCreateInstance+0xf7
0006c348 77a806aa 77b765d4 00000000 0006c540 ole32!CClientContextActivator::CreateInstance+0x8f
0006c388 77a805dc 0006c540 00000000 0006ca8c ole32!ActivationPropertiesIn::DelegateCreateInstance+0xf7
0006cb38 77a64eb1 000a2f08 00000000 00000001 ole32!ICoCreateInstanceEx+0x3c9
0006cb60 77a64e80 000a2f08 00000000 00000001 ole32!CComActivator::DoCreateInstance+0x28
0006cb84 77a65102 000a2f08 00000000 00000001 ole32!CoCreateInstanceEx+0x1e
0006cbb4 779d69a5 000a2f08 00000000 00000001 ole32!CoCreateInstance+0x37
0:002> ~
0 Id: 9bc.b58 Suspend: 1 Teb: 7ffdf000 Unfrozen
1 Id: 9bc.8a4 Suspend: 1 Teb: 7ffde000 Unfrozen
. 2 Id: 9bc.d4 Suspend: 1 Teb: 7ffdd000 Unfrozen
That’s it. I’ll post again in the near future around how you can build on what I’ve just walked through here in order to debug managed code with WinDbg and take advantage of features within the debugger that don’t (yet) appear elsewhere.
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
Continuing on from the previous post on using WinDbg let’s take what we learnt in that previous post and apply it to managed code.
WinDbg supports the debugging of managed code through an extension named SOS.DLL. This is named for esoteric reasons that I’ll not get into here but you can find out where that name came from by Googling the web for it.
This DLL is shipped with the latest debugger so you’ll find a version of it which matches V1.1 of the .NET framework in the debugger’s installation folder and you’ll also find a sub-folder labelled CLR10 which contains the previous version of the DLL.
You interact with the SOS.DLL extension by loading it up into WinDbg as you load any other extension. So, if I want to make use of SOS.DLL I issue;
.load SOS.DLL
And the debugger loads the extension for me and I can see it in the results of a;
.chain
command and I can set it as my default extension by using;
.setdll SOS.DLL
Before we go into SOS.DLL in more depth I’ll add a quick word about the symbols for the .NET framework. If you have the .NET framework SDK installed on your machine then you should have symbols for DLLs such as mscorwks.dll and mscoree.dll which make up the CLR installed onto your machine. On my machine (with VS.NET 2003 installed) these symbol files exist in a folder named;
C:/program files/Microsoft Visual Studio.NET 2003/SDK/v1.1/Symbols
So, this means that you can add this folder to your symbol path (see the previous post) and have WinDbg pick up symbols for the CLR bits for you locally. Alternatively, these symbols will come down from the symbol server anyway but it might save you some download time.
So, let’s consider that straight away what that adds. If I have a piece of managed code such as the following;
using System;
using System.Threading;
namespace ConsoleApplication12
{
class BadClass
{
public BadClass()
{
}
~BadClass()
{
Thread.Sleep(Timeout.Infinite);
}
}
class EntryPoint
{
static void Main(string[] args)
{
BadClass c1 = new BadClass();
BadClass c2 = new BadClass();
System.GC.Collect();
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
Console.WriteLine("Done");
Console.ReadLine();
}
}
}
Then I’ve created a bit of a monster in that what you should find is that this piece of code probably runs to completion if you run it in debug mode but it’ll never complete if you run it in release mode. This is because in release mode we made c1 and c2 eligible for garbage collection at an earlier point and that allows this code to run the finalizer for BadClass and that finalizer blocks the finalizer thread.
How to debug this with WinDbg? Run the process in release mode, attach WinDbg and have a look at the stack traces with a;
~* kb 200
command. I can see straight away from this that one of my threads looks like this;
2 Id: 7b8.a08 Suspend: 1 Teb: 7ffdd000 Unfrozen
ChildEBP RetAddr Args to Child
06bffd50 7c90fb74 7c8023ae 00000001 06bffd84 ntdll!KiFastSystemCallRet
06bffd54 7c8023ae 00000001 06bffd84 00000000 ntdll!NtDelayExecution+0xc
06bffdac 7929703b ffffffff 00000001 ffffffff KERNEL32!SleepEx+0x61
06bffdcc 792e42d5 ffffffff 06bffe04 06bffe34 mscorwks!Thread::UserSleep+0x93
06bffddc 0095a492 06bffde8 ffffffff 06bffe3c mscorwks!ThreadNative::Sleep+0x30
WARNING: Frame IP not in any known module. Following frames may be wrong.
06bffe34 791f9118 06bffeb0 7920fc3f ffffffff 0x95a492
06bffec0 791f91dd 04a619a4 00158010 00147880 mscorwks!MethodTable::CallFinalizer+0xee
06bffed4 791e1f08 04a619a4 7c8099f2 00000000 mscorwks!CallFinalizer+0x84
06bfff6c 791bbed0 04a619a4 00000000 0012fb9c mscorwks!CallFinalizer+0x245
06bfffb4 7c812ccb 00000000 0012fb9c 003e0000 mscorwks!GCHeap::FinalizerThreadStart+0xc2
06bfffec 00000000 791d0020 00000000 00000000 KERNEL32!BaseThreadStart+0x37
So, I can see that this is my finalizer thread and somewhere in the call chain I can see that this thread is calling Sleep which sounds like a bad idea to me for a finalizer method.
Instantly, you get a good picture of what might be going on here. As an aside, it’s worth taking a look around the mscorwks, mscoree and friends DLLs to have a look at places where you can set breakpoints. You can do this with;
X mscoree!*
X mscorwks!*
So, our example so far has been nice but it doesn’t directly interact with managed code. If we want to do that then we need to look to the SOS.DLL and the functionality that it offers. We can see this with !SOS.help which gives;
0:003> !sos.help
SOS : Help
COMState | List COM state for each thread
ClrStack | Provides true managed stack trace, source and line numbers.
Additional parameters: -p[arams] -l[ocals] -r[egs] -a[ll].
DumpClass
DumpDomain [
DumpHeap [-stat] [-min 100] [-max 2000] [-mt 0x3000000] [-type
DumpMD
DumpMT [-MD]
DumpModule
DumpObj
DumpStack [-EE] [-smart] [top stack [bottom stack] | -EE only shows managed stack items.
DumpStackObjects [top stack [bottom stack]
DumpVC
EEHeap [-gc] [-win32] [-loader] | List GC/Loader heap info
EEStack [-short] [-EE] | List all stacks EE knows
EEVersion | List mscoree.dll version
FinalizeQueue [-detail] | Work queue for finalize thread
GCInfo [
GCRoot
IP2MD
Name2EE
ObjSize [
ProcInfo [-env] [-time] [-mem] | Display the process info
RWLock [-all]
SyncBlk [-all|#] | List syncblock
ThreadPool | Display CLR threadpool state
Threads | List managed threads
Token2EE
u [
So, there are some simpler “global-state” commands in here which show us information about the CLR. These would be things such as;
!ProcInfo
0:003> !ProcInfo
---------------------------------------
Process Times
Process Started at: 2004 Aug 3 12:5:20.76
Kernel CPU time : 0 days 00:00:00.03
User CPU time : 0 days 00:00:00.04
Total CPU time : 0 days 00:00:00.07
---------------------------------------
Process Memory
WorkingSetSize: 7476 KB PeakWorkingSetSize: 7476 KB
VirtualSize: 142832 KB PeakVirtualSize: 142832 KB
PagefileUsage: 2916 KB PeakPagefileUsage: 2916 KB
---------------------------------------
32 percent of memory is in use.
Memory Availability (Numbers in MB)
Total Avail
Physical Memory 2046 1372
Page File 3944 3350
Virtual Memory 2047 1907
!Threads
0:003> !Threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
PreEmptive GC Alloc Lock
ID ThreadOBJ State GC Context Domain Count APT Exception
0 cd0 0014cf38 2000020 Enabled 00000000:00000000 00147880 0 Ukn
2 a08 00158010 2001220 Enabled 04a61d94:04a61ff4 00147880 0 Ukn (Finalizer)
!ThreadPool
0:003> !ThreadPool
Worker Thread: Total: 0 Running: 0 Idle: 0 MaxLimit: 0 MinLimit: 0
Work Request in Queue: 0
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread: Total: 0 Free: 0 MaxFree: 0 CurrentLimit: 0 MaxLimit: 1000 MinLimit: 0
!COMState
0:003> !COMState
ID TEB APT APTId CallerTID Context
0 cd0 7ffdf000 Ukn
1 e3c 7ffde000 Ukn
2 a08 7ffdd000 Ukn
3 d88 7ffdc000 Ukn
The last one here, COMState, is useful for seeing how your .NET threads have initilaised themselves to COM (i.e. are they STA threads or MTA threads and more detailed info on where they are with respect to COM apartments, contexts, etc).
!FinalizeQueue
0:003> !FinalizeQueue
SyncBlock to be cleaned up: 0
----------------------------------
generation 0 has 0 finalizable objects (0014d330->0014d330)
generation 1 has 0 finalizable objects (0014d330->0014d330)
generation 2 has 0 finalizable objects (0014d330->0014d330)
Ready for finalization 1 objects (0014d330->0014d334)
Statistics:
MT Count TotalSize Class Name
935108 2 24 ConsoleApplication12.BadClass
Total 2 objects
In our scenario you can get a clear picture of what’s present on the queue for finalization in that we have 2 instances of BadClass resident in there.
!EEVersion
0:003> !EEVersion
1.1.4322.573 retail
Workstation build
We can get a picture of the “global” state of memory within the application through the dumpheap command.
The simplest way to use dumpheap is to provide the –stat option which will give a complete list of what’s allocated on the managed heap. Note that for a big process you might need to go and get a cup of tea whilst this list is being created. In our small example dumpheap –stat gives us;
total 57 objects
Statistics:
MT Count TotalSize Class Name
79c077b4 1 20 System.Collections.ArrayList
79bff0a4 1 20 System.AppDomainSetup
79c19424 1 24 System.LocalDataStoreMgr
935108 2 24 ConsoleApplication12.BadClass
79bfcb44 1 32 System.SharedStatics
79c0d0cc 1 52 System.Collections.Hashtable
95236c 1 56 System.Char[]
79bfc414 1 64 System.ExecutionEngineException
79bfc2dc 1 64 System.StackOverflowException
79bfc1a4 1 64 System.OutOfMemoryException
9526b0 1 76 System.Byte[]
79bfdbec 1 80 System.AppDomain
952c28 1 144 System.Collections.Hashtable/bucket[]
952964 1 292 System.Int32[]
79bf9af8 26 1448 System.String
14d0f0 11 2800 Free
95209c 5 6328 System.Object[]
Total 57 objects
So we can see that we have 57 objects here. If you examine the line for our managed type BadClass you’ll see that it has a MT column for its method table. This effectively describes the type and we can use it to restrict the output of dumpheap by doing;
!dumpheap –mt 935108
0:003> !dumpheap -mt 935108
Address MT Size
04a61998 00935108 12
04a619a4 00935108 12
total 2 objects
Statistics:
MT Count TotalSize Class Name
935108 2 24 ConsoleApplication12.BadClass
Total 2 objects
Note that this actually gives us addresses of the instances of our type (2 in our case) which we’ll come back to in a second but, first, how would we get the MethodTable for a particular type without sitting through !dumpheap –stat first? We can use the Name2EE function as below;
!Name2EE ConsoleApplication12.exe ConsoleApplication12.BadClass
0:003> !Name2EE ConsoleApplication12.exe ConsoleApplication12.BadClass
--------------------------------------
MethodTable: 00935108
EEClass: 06c2334c
Name: ConsoleApplication12.BadClass
And now we know the MethodTable address for our type which is nice J Now, coming back to those addresses. These allow us to dump out the objects themselves using the !dumpobj command;
!dumpobj 04a61998
0:003> !dumpobj 04a61998
Name: ConsoleApplication12.BadClass
MethodTable 0x00935108
EEClass 0x06c2334c
Size 12(0xc) bytes
mdToken: 02000002 (t:/Temp/ConsoleApplication12/bin/Release/ConsoleApplication12.exe)
This object isn’t so interesting so I’ve gone back and dumped out a hashtable so you can see what a real object looks like;
0:003> !dumpobj 04a61a90
Name: System.Collections.Hashtable
MethodTable 0x79c0d0cc
EEClass 0x79c0d20c
Size 52(0x34) bytes
mdToken: 020000f9 (c:/windows/microsoft.net/framework/v1.1.4322/mscorlib.dll)
FieldDesc*: 79c0d270
MT Field Offset Type Attr Value Name
79c0d0cc 4000395 4 CLASS instance 04a61d04 buckets
79c0d0cc 4000396 1c System.Int32 instance 0 count
79c0d0cc 4000397 20 System.Int32 instance 0 occupancy
79c0d0cc 4000398 24 System.Int32 instance 7 loadsize
79c0d0cc 4000399 28 System.Single instance 0.720000 loadFactor
79c0d0cc 400039a 2c System.Int32 instance 0 version
79c0d0cc 400039b 8 CLASS instance 00000000 keys
79c0d0cc 400039c c CLASS instance 00000000 values
79c0d0cc 400039d 10 CLASS instance 00000000 _hcp
79c0d0cc 400039e 14 CLASS instance 00000000 _comparer
79c0d0cc 400039f 18 CLASS instance 00000000 m_siInfo
79c0d0cc 4000394 0 CLASS shared static primes
>> Domain:Value 00147880:04a61ac4 <<
So, in essence, we can traverse the entire managed heap from within WinDbg here and we can see the details of every single instance that is stored on that managed heap and we can track down all the members of those instances ad infinitum. This is really, really powerful stuff.
What about stack frames? Well, if we look at the original stack trace that I had when I was trying to diagnose my “hung application” problem we can see the frames look like this;
2 Id: 7b8.a08 Suspend: 1 Teb: 7ffdd000 Unfrozen
ChildEBP RetAddr Args to Child
06bffd50 7c90fb74 7c8023ae 00000001 06bffd84 ntdll!KiFastSystemCallRet
06bffd54 7c8023ae 00000001 06bffd84 00000000 ntdll!NtDelayExecution+0xc
06bffdac 7929703b ffffffff 00000001 ffffffff KERNEL32!SleepEx+0x61
06bffdcc 792e42d5 ffffffff 06bffe04 06bffe34 mscorwks!Thread::UserSleep+0x93
06bffddc 0095a492 06bffde8 ffffffff 06bffe3c mscorwks!ThreadNative::Sleep+0x30
WARNING: Frame IP not in any known module. Following frames may be wrong.
06bffe34 791f9118 06bffeb0 7920fc3f ffffffff 0x95a492
06bffec0 791f91dd 04a619a4 00158010 00147880 mscorwks!MethodTable::CallFinalizer+0xee
06bffed4 791e1f08 04a619a4 7c8099f2 00000000 mscorwks!CallFinalizer+0x84
06bfff6c 791bbed0 04a619a4 00000000 0012fb9c mscorwks!CallFinalizer+0x245
06bfffb4 7c812ccb 00000000 0012fb9c 003e0000 mscorwks!GCHeap::FinalizerThreadStart+0xc2
06bfffec 00000000 791d0020 00000000 00000000 KERNEL32!BaseThreadStart+0x37
So, what’s the bit that’s highlighted there? How come WinDbg can’t fathom this stack frame? Essentially what this points to is that in between the mscorwks!MethodTable::CallFinalizer function and mscorwks!ThreadNative::Sleep function we have managed code. If we want to see the managed stack frames then we can issue;
!ClrStack
0:002> !ClrStack
Thread 2
ESP EIP
06bffe04 7c911444 [FRAME: ECallMethodFrame] [DEFAULT] Void System.Threading.Thread.Sleep(I4)
06bffe14 06d4010e [DEFAULT] [hasThis] Void ConsoleApplication12.BadClass.Finalize()
at [+0x1e] [+0x6]
which works on the current thread (i.e. the one that the debugger is “focused” on) and dumps out the managed stack – you can see that we now could guess that we have a problem in BadClass.Finalize which we kind of knew all along ;-)
We can also do !ClrStack –params –locals –regs –all to include parameter, local and register information which is going to be really helpful for real-world debugging. For managed objects that we find we can then go and do !dumpobj to take a look at those objects.
Setting breakpoints isn’t quite as easy for managed code in WinDbg as it might be but it’s far from impossible. What we have to work out to set a breakpoint on a manged function is the address of the code for that function.
For our type, BadClass let’s say that we want to set a breakpoint in its constructor. What we do is;
1) Get the MethodTable address for the type by issuing our !Name2EE command.
a. !Name2EE ConsoleApplication12.exe ConsoleApplication12.BadClass
0:002> !Name2EE ConsoleApplication12.exe ConsoleApplication12.BadClass
--------------------------------------
MethodTable: 00935108
EEClass: 06c2334c
Name: ConsoleApplication12.BadClass
2) We can now get hold of the method table for this class by using the !dumpmt command as below;
a. !dumpmt –md 00935108
0:002> !dumpmt -md 00935108
EEClass : 06c2334c
Module : 0015b610
Name: ConsoleApplication12.BadClass
mdToken: 02000002 (t:/Temp/ConsoleApplication12/bin/Release/ConsoleApplication12.exe)
MethodTable Flags : 180000
Number of IFaces in IFaceMap : 0
Interface Map : 00935148
Slots in VTable : 5
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
79bf84eb 79bf84f0 None [DEFAULT] [hasThis] String System.Object.ToString()
79bf8473 79bf8478 None [DEFAULT] [hasThis] Boolean System.Object.Equals(Object)
79bf848b 79bf8490 None [DEFAULT] [hasThis] I4 System.Object.GetHashCode()
009350fb 00935100 None [DEFAULT] [hasThis] Void ConsoleApplication12.BadClass.Finalize()
009350eb 009350f0 None [DEFAULT] [hasThis] Void ConsoleApplication12.BadClass..ctor()
3) We can see that the address for our constructor here is 009350eb. We can provide this as a breakpoint to WinDbg
a. bp 009350eb
b. and when that breakpoint hits we can issue a step command (“t”) and we’ll find that we’re inside the function that we wanted to break in.
Last topic for this post – how to determine what is holding on to your CLR memory. I talked about how we can use !dumpheap to take a look at what’s on the managed heap but we can also use the fantastic !gcroot command in order to determine for any managed object what “set of roots” are actually causing that object to remain “alive” rather than be garbage collected by the GC.
This is a particularly useful command if you suspect you’ve got a memory leak somewhere in the sense that something is holding on to a managed object longer than it should be – you can break into the process with WinDbg and take a look at what “roots” are present for your particular object and, hopefully, that’d move you forward in determining what’s going on.
An example of the output here on !gcroot is as below let’s use the following piece of code;
class EntryPoint
{
private static ArrayList al = new ArrayList();
static void Main(string[] args)
{
for (int i = 0; i < 10; ++i)
{
switch (i % 3)
{
case 0:
al.Add("Hello");
break;
case 1:
al.Add(new Exception(""));
break;
case 2:
al.Add(new object());
break;
}
}
Console.ReadLine();
}
}
So, if I want to know what objects are around at the point of the Console.ReadLine() call then I can use !dumpheap –stat to show me;
total 132 objects
Statistics:
MT Count TotalSize Class Name
79c53524 1 12 System.IO.TextReader/NullTextReader
79c0c46c 1 12 System.__Filters
79c0bfa8 1 12 System.Reflection.Missing
79c03458 1 12 System.IO.Stream/NullStream
79c8d504 1 16 System.IO.TextReader/SyncTextReader
79c58114 1 20 System.Text.CodePageEncoding/Decoder
79c3667c 1 20 System.Text.UTF8Encoding/UTF8Encoder
79c363c4 1 20 System.IO.TextWriter/NullTextWriter
79c077b4 1 20 System.Collections.ArrayList
79c03efc 1 20 System.Text.UTF8Encoding
79bff0a4 1 20 System.AppDomainSetup
79c61b94 2 24 System.Text.Encoding/DefaultEncoder
79bfcb44 1 32 System.SharedStatics
79bf8364 3 36 System.Object
79c36114 2 40 System.Text.CodePageEncoding
79bfb4f4 2 40 System.Text.StringBuilder
79c61cac 2 48 System.IO.TextWriter/SyncTextWriter
79c57f8c 1 56 System.IO.StreamReader/NullStreamReader
79c53fac 1 56 System.IO.StreamReader
79c618d4 3 60 System.IO.__ConsoleStream
79c0b37c 4 64 System.RuntimeType
79bfc414 1 64 System.ExecutionEngineException
79bfc2dc 1 64 System.StackOverflowException
79bfc1a4 1 64 System.OutOfMemoryException
79bfdbec 1 80 System.AppDomain
79c0c584 3 84 System.Reflection.MemberFilter
79c35d24 3 156 System.IO.StreamWriter
79bfbcdc 3 192 System.Exception
9526b0 6 1356 System.Byte[]
95236c 12 2008 System.Char[]
79bf9af8 56 3280 System.String
14d0f0 6 10220 Free
95209c 7 10440 System.Object[]
Total 132 objects
And if I now want to know what instances of System.Exception are around I can do !dumpheap –mt 79bfbcdc and get;
0:003> !dumpheap -mt 79bfbcdc
Address MT Size
04a619fc 79bfbcdc 64
04a61a48 79bfbcdc 64
04a61a94 79bfbcdc 64
total 3 objects
Statistics:
MT Count TotalSize Class Name
79bfbcdc 3 192 System.Exception
Total 3 objects
And if I now want to know why the first of these instances is still around I can use !gcroot on it to give me;
0:003> !gcroot 04a619fc
Scan Thread 0 (ec4)
ESP:12f644:Root:04a6197c(System.Collections.ArrayList)->04a61990(System.Object[])->04a619fc(System.Exception)
ESP:12f658:Root:04a6197c(System.Collections.ArrayList)->04a61990(System.Object[])
ESP:12f668:Root:04a6197c(System.Collections.ArrayList)->04a61990(System.Object[])
Scan Thread 2 (eb8)
Scan HandleTable 15f008
Scan HandleTable 149c88
HANDLE(Strong):9411c0:Root:05a62000(System.Object[])->04a6197c(System.Collections.ArrayList)
I can see that my System.Exception instance at 04a619fc is being referenced (rooted) by an array of objects at 04a61990 and that is rooted by an ArrayList at 04a619c.
So, we can chase back object references as far as we need to work out what’s going on with our managed memory and why particular instances are not yet eligible for GC.
The WinDbg debugger does not know anything about managed exceptions. Whenever managed code throws an exception it throws a native exception of type 0xe0434f4d
If you want to stop on managed exceptions then you can issue to the debugger;
sxe 0xe0434f4d
or
sxe clr (much easier!)
and it’ll break at the point where the exception is thrown. You can grab the exception record from an unmanaged point of view by issuing a display exception record command with a minus 1 as;
.exr -1
And this will give you a result such as;
0:000> .exr -1
ExceptionAddress: 7c82dad4 (KERNEL32!RaiseException+0x00000038)
ExceptionCode: e0434f4d (CLR exception)
ExceptionFlags: 00000001
NumberParameters: 0
I think there’s a nice way of turning this into a managed exception object but, at the time of writing, I can’t work it out. What you can do is to execute a !dumpstackobjects and the usual case would be to should find your exception living within the set of objects that you get back there.
That’s it for this posting – if you want a more info on debugging with SOS.DLL check out this big guide that’s over on MSDN which will give you more info and also have a look at resources such as;
http://blogs.msdn.com/mvstanton/archive/2004/04/05/108023.aspx
http://msdn.microsoft.com/msdnmag/issues/03/06/Bugslayer/default.aspx
Enjoy!
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
I can’t get off this debugging topic at the moment.
To follow up briefly on the postings that I put up about WinDbg and the SOS.DLL I wanted to add some information about Visual Studio because I think I goofed really with the last couple of postings on that topic.
You learn something new every day and I just learnt some stuff about Visual Studio. It turns out that Visual Studio can load up the SOS.DLL extension with consummate ease. I'd thought that this was a 2005 feature but it turns out that it’s in Visual Studio 2003 as well so that's a major bonus.
So, if you're keen to play with SOS.DLL you can work happily with it inside of Visual Studio and you don't have to worry so much about going and getting WinDbg unless you want some of its more advanced features.
How do you make this work?
Firstly, you need to make sure that the debugging project that you’re working in has support for unmanaged debugging. You can do this in your project properties (Configuration Properties->Debugging->Enable Unmanaged Debugging) and you can also explicitly set what type of debugging you want to do if you attach the debugger to a process through the Tools->Debug Processes menu option.
Note that mixing managed/unmanaged debugging does seem to slow the debugger’s operation down quite a bit.
Once you’ve got unmanaged debugging switched on you can work with the debugger exactly as you usually do and set breakpoints and so on.
At the point where you want some SOS.DLL functionality, switch to your Immediate window (Debug->Windows->Immediate).
Put the Immediate window into immediate mode rather than command mode. You do this by typing;
immed
at the command prompt (you revert back to command mode by typing “>cmd”). You can spot command mode because it gives you a “>” prompt.
When in immediate mode type in;
.load sos
And there you go. Type in !EEVersion and a quick !ProcInfo so that you feel at home and you’re off on your extended debugging session.
Note that within Visual Studio, commands such as !ClrStack work on the thread that you have selected within the Threads window – i.e. exactly as you’d expect.
The beta version of Visual Studio 2005 also loads up SOS.DLL in the same way.
In V2.0 it looks like some new commands have been added and a whole bunch of addition help features are there. Doing a !Help on V2.0 gives a command list;
You can get a (much improved) set of help information about these commands by executing;
!Help
and you even get examples of usage and output.
Some of these commands are carried over from SOS V1.1 but a lot are new. It appears that some of the commands do not work inside of Visual Studio – the 2 that I found are as below;
VMStat, VMMap
Enjoy.