Tuesday, November 23, 2010

C.L.R :- .NET Debugging

If you ever get curious enough to know how .Net debugging works,  there is an excellent source of knowledge :-                     http://blogs.msdn.com/b/jmstall/.

I got curious and found excellent posts from the insider Mike Stall , one of previous team member of Microsoft Managed Debugging Team. My curiosity lead me to  unmanaged api ICorDebug  (COM api). Next I was in search of managed wrapper around the CorDebug.idl, my search lead me to Managed Stack Explorer, a command line managed debugging tool . You can get your hands on source code at  http://mse.codeplex.com/.  Once you get hold of code, you get all the managed wrappers (C#) around the COM api of ICorDebug . Wrappers are manually created using ComImport   attributes in C# , so you do not have to go through laborious process of creating each and every wrapper around classes, interfaces and structures.  Apart from manually created wrappers , you also get p-invoke methods from mscoree.dll within the source code:--

[DllImport("mscoree.dll", CharSet=CharSet.Unicode, PreserveSig=false)]
public static extern Debugger.Interop.CorDebug.ICorDebug CreateDebuggingInterfaceFromVersion(int debuggerVersion, string debuggeeVersion);

[DllImport("mscoree.dll", CharSet=CharSet.Unicode)]
public static extern int GetCORVersion([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder szName, Int32 cchBuffer, out Int32 dwLength);

These dll-import method gets the current ICorDebug version and creates the ICorDebug interface which is encapsulated within the ICorDebug managed wrappers.  You have to call Initialize() and SetManagedHandler()  on the ICorDebug managed wrapper , to initialize the ICorDebug interface and set the managed call-back interface for all the call-backs returned from the ICorDebug interface.  ICorDebugManagedCallback interface is also the wrapper around the com interface.

ICorDebug rawDebuggingAPI;
          rawDebuggingAPI = NativeMethods.CreateDebuggingInterfaceFromVersion((int)CorDebuggerVersion,debuggerVersion);

rawDebuggingAPI .Initialize ();
           rawDebuggingAPI .SetManagedHandler (new ManagedCallback(this));

When ever we want to start debugging, we create ICorDebug interface and spawn out a CorDebug process from the call to CreateProcess() method on ICorDebug interface providing the information of the debugee process .

ICorDebugProcess process = CorDebug.CreateProcess(
                            filename,   // lpApplicationName
                              // If we do not prepend " ",

                           " " + arguments,                       // lpCommandLine
                            ref secAttr,                       // lpProcessAttributes
                            ref secAttr,                      // lpThreadAttributes
                            1,//TRUE                    // bInheritHandles
                            0x00000010 /*CREATE_NEW_CONSOLE*/,    // dwCreationFlags
                            IntPtr.Zero,                       // lpEnvironment
                            workingDirectory,                       // lpCurrentDirectory
                            (uint)pprocessStartupInfo,        // lpStartupInfo
                            (uint)pprocessInfo,               // lpProcessInformation,
                            CorDebugCreateProcessFlags.DEBUG_NO_SPECIAL_OPTIONS   // debuggingFlags
                            );

or we can attach to existing process by calling DebugActiveProcess method on ICorDebug interface which returns the ICorDebugProcess.

m_debugger.DebugActiveProcess ((uint)processId, win32Attach ? 1 : 0, out proc);

Whatever method we choose to start debugee process under debugger , the call backs from the creation of debugee process are send to us via ICoreDebugManagedCallback. There are all kinds of call-backs related to debugging  that we get  (LoadAppDomian,  LoadModule, CreateProcess,  Breakpoint, StepComplete,EditAndContinue etc).  Using the call-backs we can keep track of all the ICorDebug elements for current session.

Call-back also includes notification for the creation of ICorDebugThread . We can keep the track of this thread and can utilize this com wrapper to get the hold of current stack frames. We can call EnumerateChains method on ICorDebugThread and loop through the ICorDebugChain element. ICorDebugChain represent the one segment of call stack for ICorDebugThread and consists of ICorDebugFrame collection. ICorDebugFrame is the stack frame for current call stack segment (ICorDebugChain). We can get the collection of stack frames by invoking the method EnumerateFrames on ICorDebugChain element.

 

uint corChainIndex = CorThread.EnumerateChains().GetCount();
                foreach(ICorDebugChain corChain in CorThread.EnumerateChains().GetEnumerator()) {
                    corChainIndex--;
                    uint corFrameIndex = corChain.EnumerateFrames().GetCount();
                    foreach(ICorDebugFrame corFrame in corChain.EnumerateFrames().GetEnumerator()) {
                        corFrameIndex--;
                  try {
                         ------------

                    } catch (COMException) {

                            continue;
                        };
                        yield return corFrame ;
                    }
                }

Once we get ICorDebugFrame from the ICorDebugChain, we get the method or function (ICorDebugFunction ) at that frame by calling GetFunction() on ICorDebugFrame element.

ICorDebugFunction func = corFrame .GetFunction();

Setting breakpoint on specified line number:--------------

From ICorDebugManagedCallBack we also get the notification of the current module through ICorDebugModule parameter from LoadModule call-back.  ICorDebugModule contains the method GetMetaDataInterface() for retrieving the IMetaDataImport interface which is the interface that provides the metadata information for the current ICorDebugModule . Having got the metadata interface next step is to create the CorSymBinder_SxSClass instance and cast it into ISymUnmanagedBinder interface. Now we can call the method GetReaderForFile that takes the path of pdb file for current module and metadata interface as parameters to return ISymUnmanagedReader. This method opens the pdb file and returns the ISymUnmanagedReader , which is responsible for reading the debugging symbols from the pdb file.

Guid guid = new Guid("{ 0x7dac8207, 0xd3ae, 0x4c75, { 0x9b, 0x67, 0x92, 0x80, 0x1a, 0x49, 0x7d, 0x44 } }");
  IMetaDataImport     metaData = (IMetaDataImport)pModule.GetMetaDataInterface(ref guid);

ISymUnmanagedBinder symBinder = new Debugger.Interop.CorSym.CorSymBinder_SxSClass();
  ISymUnmanagedReader reader   =  symBinder.GetReaderForFile(metaData, fullname, searchPath);

Now we can get ISymUnmanagedDocument collection from the ISymUnmanagedReader by calling GetDocuments method on ISymUnmanagedReader. ISymUnmanagedDocument is the document referenced by the pdb symbol store.

Through ISymUnmanagedDocument we can get the closest line to the sequencepoint , providing our line number for the function that we want to debug. Sequencepoints are the places that IL marks as the points that can be used for stopping and debugging purpose. We do so by invoking method FindClosestLine on ISymUnmanagedReader providing our line number.

FindClosestLine returns the closest line number to the relevant Sequencepoint in IL corresponding to our provided line  number. Using this line number we can get the function at this line number and all the sequence points for this function.

uint validLine = symDoc.FindClosestLine((uint)line);
  ISymUnmanagedMethod    symMethod = symReader.GetMethodFromDocumentPosition(symDoc, (uint)validLine, (uint)column);

SequencePoint[] seqPoints = symMethod.GetSequencePoints();

Now we can iterate through all the sequence points and get the ICorDebugFunction .

ICorDebugFunction func =  pModule.GetFunctionFromToken(symMethod.GetToken());

On this ICorDebugFunction we can place our breakpoint.

ICorDebugFunctionBreakpoint corBreakpoint = func .GetILCode().CreateBreakpoint((uint)ILStart);

ILStart parameter:- we get it through sequencepoint

int ILStart=(int)sqPoint.Offset;

Above code was taken by me  for study purpose from Managed Stack Explorer and SharpDevelop Debugging Addin  source code.

ICorDebug is feature rich debugging api, so it has numerous functionalities that I have yet to cover. Hope this blog will be useful.

No comments:

Post a Comment