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:
public int GetTypeTokenFromName(string name)
{
IMetadataImport importer = GetMetadataInterface<IMetadataImport>();
int token = TokenNotFound;
if (name.Length == 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
):
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:
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();
}
...
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 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):
if(managedModule.SymReader==null)
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)
{
}
}
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:
Int32 line = 0;
try
{
line = symdoc.FindClosestLine(lineNumber);
}
catch (System.Runtime.InteropServices.COMException ex)
{
if (ex.ErrorCode == (Int32)HResult.E_FAIL)
continue;
}
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):
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:
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+)$");
...
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;
}
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