Writing a .net debugger (part 4) – breakpoints


After the last part the mindbg debugger stops at the application entry point, has module symbols loaded and displays source code that is being executed. Today we will gain some more control over the debugging process by using breakpoints. By the end of this post we will be able to stop the debugger on either a function execution or at any source line.

Setting a breakpoint on a function is quite straightforward – you only need to call CreateBreakpoint method on a ICorDebugFunction instance (that we want to have a stop on) and then activate the newly created breakpoint (with ICorDebugBreakpoint.Activate(1) function). The tricky part is how to find the ICorDebugFunction instance based on a string provided by the user. For this purpose we will write few helper methods that will use ICorMetadataImport interface. Let’s assume that we would like to set a breakpoint on a Test method of TestCmd class in testcmd.exe assembly. We will then use command “set-break mcmdtest.exe!TestCmd.Test”. After splitting the command string we will receive module path, class name and method name. We could easily find a module with a given path (we will iterate through the modules collection – for now it won’t be possible to create a breakpoint on a module that has not been loaded). Having found a module we may try to identify the type which “owns” the method. I really like the way how it is done in mdbg source code so we will copy their idea :). We will add a new method to the CorModule class:

// Brilliantly written taken from mdbg source code.
// returns a type token from name
// when the function fails, we return token TokenNotFound value.
public int GetTypeTokenFromName(string name)
{
    IMetadataImport importer = GetMetadataInterface<IMetadataImport>();

    int token = TokenNotFound;
    if (name.Length == 0)
        // this is special global type (we'll return token 0)
        token = TokenGlobalNamespace;
    else
    {
        try
        {
            importer.FindTypeDefByName(name, 0, out token);
        }
        catch (COMException e)
        {
            token = TokenNotFound;
            if ((HResult)e.ErrorCode == HResult.CLDB_E_RECORD_NOTFOUND)
            {
                int i = name.LastIndexOf('.');
                if (i > 0)
                {
                    int parentToken = GetTypeTokenFromName(name.Substring(0, i));
                    if (parentToken != TokenNotFound)
                    {
                        try
                        {
                            importer.FindTypeDefByName(name.Substring(i + 1), parentToken, out token);
                        }
                        catch (COMException e2)
                        {
                            token = TokenNotFound;
                            if ((HResult)e2.ErrorCode != HResult.CLDB_E_RECORD_NOTFOUND)
                                throw;
                        }
                    }
                }
            }
            else
                throw;
        }
    }
    return token;
}

Then, we will implement the MetadataType class that will inherit from Type. For clarity I will show you only the impelmented methods (others throw NotImplementedException):

internal sealed class MetadataType : Type
{
    private readonly IMetadataImport p_importer;
    private readonly Int32 p_typeToken;

    internal MetadataType(IMetadataImport importer, Int32 typeToken)
    {
        this.p_importer = importer;
        this.p_typeToken = typeToken;
    }

    ...
    
    public override System.Reflection.MethodInfo[] GetMethods(System.Reflection.BindingFlags bindingAttr)
    {
        IntPtr hEnum = new IntPtr();
        ArrayList methods = new ArrayList();

        Int32 methodToken;
        try
        {
            while (true)
            {
                Int32 size;
                p_importer.EnumMethods(ref hEnum, p_typeToken, out methodToken, 1, out size);
                if (size == 0)
                    break;
                methods.Add(new MetadataMethodInfo(p_importer, methodToken));
            }
        }
        finally
        {
            p_importer.CloseEnum(hEnum);
        }
        return (MethodInfo[])methods.ToArray(typeof(MethodInfo));
    }
    
    ...
}

As you could see in the listing we also used MetadataMethodInfo. The listing below presents body of this class:

internal sealed class MetadataMethodInfo : MethodInfo
{
    private readonly Int32 p_methodToken;
    private readonly Int32 p_classToken;
    private readonly IMetadataImport p_importer;
    private readonly String p_name;


    internal MetadataMethodInfo(IMetadataImport importer, Int32 methodToken)
    {
        p_importer = importer;
        p_methodToken = methodToken;

        int size;
        uint pdwAttr;
        IntPtr ppvSigBlob;
        uint pulCodeRVA, pdwImplFlags;
        uint pcbSigBlob;

        p_importer.GetMethodProps((uint)methodToken,
                                  out p_classToken,
                                  null,
                                  0,
                                  out size,
                                  out pdwAttr,
                                  out ppvSigBlob,
                                  out pcbSigBlob,
                                  out pulCodeRVA,
                                  out pdwImplFlags);

        StringBuilder szMethodName = new StringBuilder(size);
        p_importer.GetMethodProps((uint)methodToken,
                                out p_classToken,
                                szMethodName,
                                szMethodName.Capacity,
                                out size,
                                out pdwAttr,
                                out ppvSigBlob,
                                out pcbSigBlob,
                                out pulCodeRVA,
                                out pdwImplFlags);

        p_name = szMethodName.ToString();
        //m_methodAttributes = (MethodAttributes)pdwAttr;
    }

    ...
    
    public override string Name
    {
        get { return p_name; }
    }
    
    public override int MetadataToken
    {
        get { return this.p_methodToken; }
    }
}

Finally we are ready to implement the method that will return CorFunction instance:

public CorFunction ResolveFunctionName(CorModule module, String className, String functionName)
{
    Int32 typeToken = module.GetTypeTokenFromName(className);
    if (typeToken == CorModule.TokenNotFound)
        return null;

    Type t = new MetadataType(module.GetMetadataInterface<IMetadataImport>(), typeToken);
    CorFunction func = null;
    foreach (MethodInfo mi in t.GetMethods())
    {
        if (String.Equals(mi.Name, functionName, StringComparison.Ordinal))
        {
            func = module.GetFunctionFromToken(mi.MetadataToken);
            break;
        }
    }
    return func;
}

We will now concentrate on the second type of breakpoints: the code breakpoints which are set at a specific line of the source code file. Example of usage whould be “set-break mcmdtest.cs:23″. So how to set this type of breakpoint? First we need to find a module that was built from the given source file. We will iterate through all loaded modules and if a given module has symbols loaded (SymReader property != null) then we will check its documents URLS and compare them with the requested file name (snippet based on mdbg source code):

if(managedModule.SymReader==null)
  // no symbols for current module, skip it.
  return false;

foreach(ISymbolDocument doc in managedModule.SymReader.GetDocuments())
{
  if(String.Compare(doc.URL,m_file,true,CultureInfo.InvariantCulture)==0 ||
    String.Compare(System.IO.Path.GetFileName(doc.URL),m_file,true,CultureInfo.InvariantCulture)==0)
  {
     // we will fill for body later
  }
}

Having found the module we need to find a class method that the given source line belongs to. So first let’s locate the line that is the nearest sequence point to the given line by calling ISymbolDocument.FindClosestLine method. Next, with the help of the module’s ISymbolReader we will find the ISymbolMethod instance that represents our wanted function. The last step is to get the CorFunction instance based on the method’s token:

// the upper "for" body
Int32 line = 0;
try
{
  line = symdoc.FindClosestLine(lineNumber);
}
catch (System.Runtime.InteropServices.COMException ex)
{
  if (ex.ErrorCode == (Int32)HResult.E_FAIL)
      continue; // it's not this document
}
ISymbolMethod symmethod = symreader.GetMethodFromDocumentPosition(symdoc, line, 0);
CorFunction func = module.GetFunctionFromToken(symmethod.Token.GetToken());

Code breakpoints are created using ICorDebugCode.CreateBreakpoint method which takes as its parameter a code offset at which the breakpoint should be set. We will get an instance of the ICorDebugCode from the CorFunction instance (found in the last paragraph):

// from CorFunction.cs
public CorCode GetILCode()
{
    ICorDebugCode cocode = null;
    p_cofunc.GetILCode(out cocode);
    return new CorCode(cocode);
}

Then we will find the IL offset in the function IL code that corresponds to the given source file line number:

// from CorFunction.cs
internal int GetIPFromPosition(ISymbolDocument document, int lineNumber)
{
    SetupSymbolInformation();
    if (!p_hasSymbols)
        return -1;

    for (int i = 0; i < p_SPcount; i++)
    {
        if (document.URL.Equals(p_SPdocuments[i].URL) && lineNumber == p_SPstartLines[i])
            return p_SPoffsets[i];
    }
    return -1;
}

Finally we are ready to parse user’s input and set breakpoints accordingly. I used two simple regex expressions to check the breakpoint type and call process.ResolveFunctionName for function breakpoints and process.ResolveCodeLocation for code breakpoints:

static Regex methodBreakpointRegex = new Regex(@"^((?<module>[\.\w\d]*)!)?(?<class>[\w\d\.]+)\.(?<method>[\w\d]+)$");
static Regex codeBreakpointRegex = new Regex(@"^(?<filepath>[\\\.\S]+)\:(?<linenum>\d+)$");

...

// try module!type.method location (simple regex used)
Match match = methodBreakpointRegex.Match(command);
if (match.Groups["method"].Length > 0)
{
    Console.Write("Setting method breakpoint... ");

    CorFunction func = process.ResolveFunctionName(match.Groups["module"].Value, match.Groups["class"].Value,
                                                    match.Groups["method"].Value);
    func.CreateBreakpoint().Activate(true);
    
    Console.WriteLine("done.");
    continue;
}
// try file code:line location
match = codeBreakpointRegex.Match(command);
if (match.Groups["filepath"].Length > 0)
{
    Console.Write("Setting code breakpoint...");

    int offset;
    CorCode code = process.ResolveCodeLocation(match.Groups["filepath"].Value, 
                                               Int32.Parse(match.Groups["linenum"].Value), 
                                               out offset);
    code.CreateBreakpoint(offset).Activate(true);

    Console.WriteLine("done.");
    continue;
}

I also corrected the main debugger loop so it is starting to look as a normal debugger command line :). As always the source code is available under http://mindbg.codeplex.com (revision 55832).

About these ads

I’m a web application developer keen on debugging, tracing, performance measures and .net platform internals.

Tagged with: ,
Posted in CodeProject, Using .NET debugging API
9 comments on “Writing a .net debugger (part 4) – breakpoints
  1. ellebaek says:

    Hi

    This is all very interesting, yet I can’t really figure out how to apply it to my “problem”: I would like to write a very simple tool that traces the following events in the debuggee (like a very simple/limited IntelliTrace):

    1. Method entry with parameter values.
    2. Method exit with parameter values and return value.
    3. Exceptions with the stack trace, where exception was raised and where it’ll be handled.

    For this, is it necessary to set breakpoints at the beginning and end of all methods in the debuggee in order to get the method entry/exit events? Isn’t there a generic “give me all method entry/exit events” (there is in Java)?

    Also, I’m probably a bit lame, but I can’t seem to get part 4 working with set-break commands. Could you please show an example on how to use this?

    Example:

    echo set-break Class00001.exe!Class00001.method_01 | mindbgtest c:\\Class00001.exe

    throws

    Unhandled Exception: System.InvalidOperationException: Cannot read keys when either application does not have a console or when console input has been redirected from a file. Try Console.Read.
    at System.Console.ReadKey(Boolean intercept)
    at System.Console.ReadKey()
    at mindgbtest.Program.Main(String[] args) in C:\\mindbg_part4\mindbg\mindgbtest\Program.cs:line 52

    Any pointers are most welcome.

    Thanks in advance.

    Cheers

    Finn

    • Hi Finn,

      Unfortunately method tracing is not supported directly in the managed debugging API. Using breakpoints might be a workaround here, but it will really slow down the application (especially the application start). If you can modify the binaries you should consider using PostSharp or it’s open-source alternative AfterThought. If you can’t touch the binaries think about the profiling API – though it’s a little bit harder to use.

      Now, about the issue you have. In CorDebugger.CreateProcess method I’m adding a special flag (UInt32)CreateProcessFlags.CREATE_NEW_CONSOLE. This opens a new window for the debugee process so the debugger can still use its own console window. If you don’t want to spawn a new window you need to be careful with calling Console.ReadKey from the debugger if the debuggee is running. There is probably a bug in mindbg (I will try to fix it in the nearest future) that does stop the process from running when waiting for the user input. You may check mdbg source code for a correct approach.

      Hope it helps,
      Sebastian

  2. ellebaek says:

    Hi Sebastian

    Thanks a lot for your prompt reply – very helpful and appreciated! I wasn’t aware that you could access parameter/return values through the profiling API so I’m checking that out.

    Thanks a lot.

    Cheers

    Finn

  3. ellebaek says:

    Hi again

    This CLR profiler example worked much better for my purpose:

    http://msdn.microsoft.com/en-us/magazine/cc188693.aspx

    Once again, thanks a lot for pointing me in this direction!

    Cheers

    Finn

  4. Matthias says:

    Hi Sebastian,

    great work, nicely explained.
    I have Visual C# 2010 Express and started mindbgtest with a very simple C# program (has nothing but some Console.WriteLine inside main and a Console.ReadKey to wait). The program didn’t stop at main as explained in part 3 (I tried both possibilities, creating the process and attaching to it) and didn’t load any symbols. Am I missing something ? Some options I have to set for the program I like to debug ?

    • Hi Matthias,

      Thank you for kind words. Sorry for the delay but it’s been a while since I worked on mindbg code and needed to refresh my memory:) It seems that something was changed in .NET internals and the debugger stopped working – I will need a moment to have a deeper look at it and will get back to you as soon as I figure out what’s going on there.

      Cheers,
      Sebastian

    • I think that the problem lies in DebuggingFacility.GetRuntime method. When starting the debugging process it sometimes returns an incorrect version of the runtime. I need to write some code that will check the version of the framework that the executable was created for and load it into the debugger. For now a simple workaround would be to fill the desiredVersion parameter (of the DebuggingFacility.CreateDebuggerForExeutable) to either “v4.0″ if your assembly is .NET4 or “v2.0″ for .NET2/3.5. Sorry for any inconvenience and I will try to fix it soon.

      Cheers,
      Sebastian

  5. Matthias says:

    Thanks in advance.

    Cheers

    Matthias

  6. Alejandro Mosquera says:

    Hey. You could write an example of use?

    thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 40 other followers

%d bloggers like this: