Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#

Writing a .NET Debugger (part 4) – Breakpoints

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
1 Dec 2010CPOL3 min read 15.2K   3   2
Writing a .NET debugger – 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 in which it is done in mdbg source code, so we will copy their idea :) . We will add a new method to the CorModule class:

C#
// 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 implemented methods (others throw NotImplementedException):

C#
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 the body of this class:

C#
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:

C#
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 would 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):

C#
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:

C#
// 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):

C#
// 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:

C#
// 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:

C#
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 like a normal debugger command line :) . As always, the source code is available under http://mindbg.codeplex.com (revision 55832).

Filed under: CodeProject, Debugging

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Poland Poland
Interested in tracing, debugging and performance tuning of the .NET applications.

My twitter: @lowleveldesign
My website: http://www.lowleveldesign.org

Comments and Discussions

 
Questionhow to use it ? Pin
TC-MaXX15-Nov-12 9:22
TC-MaXX15-Nov-12 9:22 
GeneralMy vote of 5 Pin
hyblusea2-Dec-10 14:04
hyblusea2-Dec-10 14:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.