Share via


Managed Debugging and inspecting Jitted code With WinDbg

WinDbg is a powerful tool to debug applications. Lately, its use has been extended to managed debugging as well through an extension called sos.dll (Son of Strike). Sos.dll is shipped with both .NET Framework 2.0 and .NET 1.1 Framework and is a WinDbg extension which allows WinDbg to read managed data structures such as Method Tables, Method Descriptors etc. The power of WinDbg is ultimate in detecting memory leaks, deadlocks and unexpected application crashes where Visual Studio debugger cannot reach.

In this post, we will try to debug and inspect the Jitted code for a very simple .NET console application. Note that WinDbg is new to me as well, so if there is any better or shorter way to do this, kindly leave a comment to me.

 namespace ConsoleApplication1
 {
     class Test
     {
         public int i;
         public int ri()
         {
             i++;
             return i;
         }
     }
     class Program
     {
         static void Main(string[] args)
         {
             Test t = new Test();
  
             int y = t.ri();
  
             Console.Write(y);
         }
     }
 }

Upon loading this ConsoleApplication1.exe into debugger, WinDbg breaks at the default debug breakpoint. At this point .NET runtime is not loaded into memory, so SOS cannot be loaded at this moment. We will set a breakpoint on Module load of mscorlib.dll, so that we can load SOS.

Entering the command sxe ld:mscorlib sets this breakpoint. Once it is set, we will go ahead with F5 to run the application. WinDbg will break upon loading on mscorlib.dll :

 ModLoad: 790c0000 79bf6000   C:\WINDOWS\assembly\NativeImages_v2.0.50727_32\mscorlib\32e6f703c114f3a971cbe706586e3655\mscorlib.ni.dll
eax=00000001 ebx=00000000 ecx=00000001 edx=00000000 esi=7ffdf000 edi=20000000
eip=7c82ed54 esp=0012e9d0 ebp=0012ea14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!KiFastSystemCallRet:
7c82ed54 c3              ret

At this point, SOS can be loaded into memory, with the command .loadby sos mscorwks

Once the SOS is loaded, we can inspect the running threads using !threads

 !threadsPDB symbol for mscorwks.dll not loaded
ThreadCount: 2
UnstartedThread: 1
BackgroundThread: 1
PendingThread: 1
DeadThread: 0
Hosted Runtime: no
                                      PreEmptive   GC Alloc           Lock
       ID OSID ThreadOBJ    State     GC       Context       Domain   Count APT Exception
   0    1  1c4 00181fd0       220 Enabled  00000000:00000000 0014cf28     1 Ukn
XXXX    2  9d4 001533b8      1400 Enabled  00000000:00000000 0014cf28     0 Ukn (Finalizer)

We can see from the output above that there is one AppDomain at 0014cf28 loaded for the current running process, which makes sense since it is a very simple application.

Lets dump the domain information using !dumpdomain 014cf28

 !dumpdomain 014cf28Domain 1: 0014cf28
LowFrequencyHeap: 0014cf4c
HighFrequencyHeap: 0014cfa4
StubHeap: 0014cffc
Stage: OPEN
SecurityDescriptor: 0014e258
Name: None

Dumping the domain gives us information about the locations of heaps. Note that it also gives the managed loaded modules in the process, but there is none listed since no managed module has been loaded yet (mscorlib is about to be loaded). We can set another breakpoint in WinDbg till the jitting engine (mscorjit.dll) is loaded using sxe ld:mscorjit

The breakpoint is hit as soon as mscorjit.dll is loaded:

 ModLoad: 79060000 790b6000   C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorjit.dll
eax=00000001 ebx=00000000 ecx=00000001 edx=00000000 esi=7ffdf000 edi=20000000
eip=7c82ed54 esp=0012e554 ebp=0012e598 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!KiFastSystemCallRet:
7c82ed54 c3              ret

Now, if we dump the domain again,

 Domain 1: 0014cf28
LowFrequencyHeap: 0014cf4c
HighFrequencyHeap: 0014cfa4
StubHeap: 0014cffc
Stage: OPEN
SecurityDescriptor: 0014e258
Name: ConsoleApplication1.exe
Assembly: 00190558 [C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 001905f0
SecurityDescriptor: 0018cdc8
  Module Name
790c2000 C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll

Assembly: 00194388 [C:\Documents and Settings\Administrator\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\bin\Release\ConsoleApplication1.exe]
ClassLoader: 00193720
SecurityDescriptor: 001942f0
  Module Name
008f2c3c C:\Documents and Settings\Administrator\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\bin\Release\ConsoleApplication1.exe

We can see that it lists, the mscorlib and our application(ConsoleApplication1.exe) as one of the loaded modules. Lets proceed onto setting the breakpoint on the Main() function. First we will need to get the address of method table of class Program by first dumping our module using !dumpmodule command. Once we get the method table, we can dump it using !dumpmt command to get the method descriptor for the Main() function.

!dumpmodule -mt 008f2c3c

 !dumpmodule -mt 008f2c3cName: C:\Documents and Settings\Administrator\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\bin\Release\ConsoleApplication1.exe
Attributes: PEFile 
Assembly: 00194388
LoaderHeap: 00000000
TypeDefToMethodTableMap: 008f00c0
TypeRefToMethodTableMap: 008f00cc
MethodDefToDescMap: 008f0118
FieldDefToDescMap: 008f012c
MemberRefToDescMap: 008f0134
FileReferencesMap: 008f017c
AssemblyReferencesMap: 008f0180
MetaData start address: 00402098 (1548 bytes)

Types defined in this module

      MT    TypeDef Name
------------------------------------------------------------------------------
008f3038 0x02000003 ConsoleApplication1.Program

Types referenced in this module

      MT    TypeRef Name
------------------------------------------------------------------------------
790fd0f0 0x01000001 System.Object

We can dump the method table of ConsoleApplication1.Program using !dumpmt -md 008f3038. The parameter -md is for dumping MethodDesc table.

 !dumpmt -md 008f3038EEClass: 008f1278
Module: 008f2c3c
Name: ConsoleApplication1.Program
mdToken: 02000003  (C:\Documents and Settings\Administrator\My Documents\Visual Studio 2005\Projects\ConsoleApplication1\ConsoleApplication1\bin\Release\ConsoleApplication1.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
79371278   7914b928   PreJIT System.Object.ToString()
7936b3b0   7914b930   PreJIT System.Object.Equals(System.Object)
7936b3d0   7914b948   PreJIT System.Object.GetHashCode()
793624d0   7914b950   PreJIT System.Object.Finalize()
008fc010   008f3028     NONE ConsoleApplication1.Program.Main(System.String[])
008fc01c   008f3030     NONE ConsoleApplication1.Program..ctor()

We can see that method descriptor for Main resides at 008f3028.

We can set the breakpoint on Program.Main() method using !bpmd -md 008f3028

 MethodDesc = 008f3028
Adding pending breakpoints...

As we run the application, the breakpoint is hit and instruction pointer is placed on the first instruction of the jitted Main() method.

 (a0c.1c4): CLR notification exception - code e0444143 (first chance)
JITTED ConsoleApplication1!ConsoleApplication1.Program.Main(System.String[])
Setting breakpoint: bp 00C20070 [ConsoleApplication1.Program.Main(System.String[])]
Breakpoint 0 hit
eax=008f3028 ebx=0012f4ac ecx=01241ec0 edx=00000000 esi=00181fd0 edi=00000000
eip=00c20070 esp=0012f484 ebp=0012f490 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
00c20070 56              push    esi

Finally, to see the generated machine code, use the Unassemble instruction to unassemble the instructions starting from current.

!u eip

    1: Normal JIT generated code
    2: ConsoleApplication1.Program.Main(System.String[])
    3: Begin 00c20070, size 35
    4: >>> 00c20070 56              push    esi
    5: 00c20071 b9b0308f00      mov     ecx,8F30B0h (MT: ConsoleApplication1.Test)
    6: 00c20076 e8a11f7dff      call    003f201c (JitHelp: CORINFO_HELP_NEWSFAST)
    7: 00c2007b 8bd0            mov     edx,eax
    8: 00c2007d ff4204          inc     dword ptr [edx+4]
    9: 00c20080 8b7204          mov     esi,dword ptr [edx+4]
   10: 00c20083 833d8c10240200  cmp     dword ptr ds:[224108Ch],0
   11: 00c2008a 750a            jne     00c20096
   12: 00c2008c b901000000      mov     ecx,1
   13: *** WARNING: Unable to verify checksum for C:\WINDOWS\assembly\NativeImages_v2.0.50727_32\mscorlib\32e6f703c114f3a971cbe706586e3655\mscorlib.ni.dll
   14: 00c20091 e8ce587478      call    mscorlib_ni+0x2a5964 (79365964) (System.Console.InitializeStdOutError(Boolean), mdToken: 06000770)
   15: 00c20096 8b0d8c102402    mov     ecx,dword ptr ds:[224108Ch]
   16: 00c2009c 8bd6            mov     edx,esi
   17: 00c2009e 8b01            mov     eax,dword ptr [ecx]
   18: 00c200a0 ff5074          call    dword ptr [eax+74h]
   19: 00c200a3 5e              pop     esi
   20: 00c200a4 c3              ret

Here are some interesting observations of this jitted code:

  • The call at line number 6 (call 003f201c (JitHelp: CORINFO_HELP_NEWSFAST) is a call to create the instance of any class. The parameter to this method is the method table address which we see being passed in previous line number in ecx. If we dump out that method table using !dumpmt , we will see that it points to class Test.

  • One might be wondering, once the Test Class is instantiated, where is the call to ri()? Interestingly, there is no call and the instructions are inlined as we see in line numbers 7 and 8. inc instruction is equivalent to i++. I do not exactly know why it has been inlined , perhaps may be because compiler optimizations were enabled in Visual Studio. Or if there are any rules governing the inlining, I do not know them yet.

  • Lines number 14 - 16 are simply initialization of Console to prepare it for emitting text using Write() method. This Write() method is finally called at line 18. If you set the breakpoint on line 18 and step through it, you will see 1 being outputted on your screen :)

Comments