Mdbg watch-trace 扩展





5.00/5 (2投票s)
我为 MDbg 调试器编写了一个简单的扩展,该扩展向其 shell 添加了一个 watch-trace (wt) 命令,允许您显示和自定义方法调用树。在这篇文章中,我将向您展示这个扩展是如何构建的以及如何使用它。
在调试过程中,您经常希望在到达停止位置之前了解调用了哪些方法以及它们的顺序。要了解这一点,您通常会使用某种分析器构建一个方法调用树并进行分析。幸运的是,如果我们手头没有分析器,我们也可以为此目的使用调试器。我为 MDbg 调试器编写了一个简单的扩展,该扩展向其 shell 添加了一个 watch-trace (wt
) 命令,允许您显示和自定义方法调用树。在这篇文章中,我将向您展示这个扩展是如何构建的以及如何使用它。
编写扩展
MDbg 是随 Windows SDK 分发的命令行调试器。它非常易于使用,并且具有不错的、可扩展的 API。前段时间,微软还发布了它的源代码——如果您对 .NET 原生调试 API 感兴趣,我强烈建议您看看它。不幸的是,MDbg 的简洁性导致它缺乏一些高级调试功能,例如:条件断点、混合模式调试或原生汇编代码支持。尽管我仍然认为它是一个出色的托管调试器,它通过 XCopy 部署,在您无法访问 Visual Studio 实例且无法设置远程调试会话的情况下尤其有用。如果您感兴趣,可以查看其MSDN 页面,其中列出了可用命令。现在让我们看看它的扩展 API。MDbg 扩展是 DLL 文件,可以使用 load
命令加载到调试器中。加载时,MDbg 会查找带有 MDbgExtensionEntryPointClass
属性的类型,然后调用其 LoadExtension
方法。所以这就是我们启动 watch-trace 扩展的方式。
namespace Mdb.Extension
{
[MDbgExtensionEntryPointClass(Url = "http://lowleveldesign.wordpress.com")]
public sealed class WatchTraceExtension : CommandBase
{
/// <summary>
/// Loads extension into the debugger.
/// </summary>
public static void LoadExtension()
{
MDbgAttributeDefinedCommand.AddCommandsFromType(
Shell.Commands, typeof(WatchTraceExtension));
WriteOutput("wtrace extension loaded");
}
...
}
神秘的突出显示行将 watch-trace 命令添加到 MDbg shell。扩展命令使用 CommandDescription
属性进行描述。CommandName
是实际命令,MinimumAbbrev
定义了可以缩写它的字符数。ShortHelp
在您执行不带参数的 help
命令时显示,而 LongHelp
将在您使用您的命令名作为参数调用 help
时显示。在我们的例子中,扩展命令将定义如下:
namespace Mdb.Extension
{
[CommandDescription(CommandName = "wtrace",
MinimumAbbrev = 2,
ShortHelp = "Watch trace command.",
LongHelp = @"Steps through the function calls, constructing a call tree.
Usage:
wt [-l <depth>] [-inc <namespace>] [-exl <namespace>]
wt -continue
-l <depth> the maximum depth of the calls to display
-inc <namespace1[,namespace2,...]> include only calls from these namespaces
(case sensitive, comma separated)
-exl <namespace[,namespace2,...]> exclude calls from this namespace
(case sensitive, comma separated)
-continue continues the last interrupted session
(you need to switch to the session thread!)")]
public static void WatchTrace(String argString)
{
...
}
}
由于我们的扩展接受一些参数,我们需要从 WatchTrace
方法的 argString
参数中提取它们。这里 ArgParser
很有帮助。它接受 argString
作为参数,允许我们指定可用于我们命令的开关列表。通过在开关名称末尾添加 :1
标记,您会告知解析器该开关需要一个参数。然后,我们可以使用 parser.GetOption
函数检索开关值,如下面的 PrepareCall
方法的代码所示:
namespace Mdb.Extension
{
[MDbgExtensionEntryPointClass(Url = "http://lowleveldesign.wordpress.com")]
public sealed class WatchTraceExtension : CommandBase
{
...
/* *** Extension options ** */
private const String depthOption = "l";
private const String incNamespacesOption = "inc";
private const String excNamespacesOption = "exc";
private const String continueOption = "continue";
/* *** Extension helpers *** */
// Parses arguments and prepares internal command variables
private static void PrepareCall(String argString)
{
ArgParser parser = new ArgParser(argString, depthOption +
":1;" + incNamespacesOption + ":1;" +
excNamespacesOption + ":1;" + continueOption);
if (parser.OptionPassed(continueOption))
{
if (depth < 0)
{
throw new MDbgShellException("No last session found.");
}
WriteOutput("Continuing the last watch-trace session...");
}
else
{
// prepare a new watch-trace session
threadId = Debugger.Processes.Active.Threads.Active.Id;
startFuncName =
Debugger.Processes.Active.Threads.Active.CurrentFrame.Function.FullName;
lastFuncName = startFuncName;
depth = 0;
if (parser.OptionPassed(depthOption))
{
maxDepth = parser.GetOption(depthOption).AsInt;
if (maxDepth < 0)
throw new MDbgShellException("Depth cannot be negative.");
}
if (parser.OptionPassed(incNamespacesOption))
{
incRgx =
GetNamespaceRegex(parser.GetOption(incNamespacesOption).AsString);
}
if (parser.OptionPassed(excNamespacesOption))
{
excRgx =
GetNamespaceRegex(parser.GetOption(excNamespacesOption).AsString);
}
}
}
// creates a regex for matching traced function names
private static Regex GetNamespaceRegex(String namespaceOption)
{
String[] tokens = namespaceOption.Split(new[] { ',' },
StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length > 0)
{
return new Regex("^(?:" + String.Join("|",
tokens.Select(s => "(?:" + s + ")")) + ")",
RegexOptions.Compiled | RegexOptions.Singleline);
}
return null;
}
...
}
}
现在我们已准备好实现我们的扩展方法体
namespace Mdb.Extension
{
[MDbgExtensionEntryPointClass(Url = "http://lowleveldesign.wordpress.com")]
public sealed class WatchTraceExtension : CommandBase
{
...
public static void WatchTrace(String argString)
{
PrepareCall(argString);
while (true)
{
if (!Debugger.Processes.HaveActive)
break;
// check call depth
if (maxDepth > 0 && depth >= maxDepth)
{
// if greater than maximum step out of the function
Debugger.Processes.Active.StepOut().WaitOne();
}
else
{
// otherwise step one instruction
Debugger.Processes.Active.StepInto(false).WaitOne();
}
MDbgFrame frame = GetCurrentFrame();
if (frame == null)
break;
String funcName = frame.Function.FullName;
if (!String.Equals(funcName, lastFuncName, StringComparison.Ordinal))
{
depth = CalculateCallDepth(frame);
if (depth < 0)
{
// it may happen if we are out of our base function
// so we need to stop tracing
break;
}
PrintCallStackTrace(funcName, depth);
lastFuncName = funcName;
}
}
// let's handle control to the debugger
Shell.DisplayCurrentLocation();
}
...
}
}
在上面的代码中,我们只是检查是否已达到最大调用深度(由用户使用 -l
开关指定),并根据此信息,我们要么退出当前函数(从而减少深度),要么进入当前函数(从而停留在其中或进入一个新函数)。GetCurrentFrame
方法在可能的情况下返回线程调用堆栈的顶层帧。
namespace Mdb.Extension
{
[MDbgExtensionEntryPointClass(Url = "http://lowleveldesign.wordpress.com")]
public sealed class WatchTraceExtension : CommandBase
{
...
private static MDbgFrame GetCurrentFrame()
{
// debuggee might be already dead
if (!Debugger.Processes.HaveActive)
return null;
// valid are only step-complete stops
Object stopReason = Debugger.Processes.Active.StopReason;
Type stopReasonType = stopReason.GetType();
if (stopReasonType != typeof(StepCompleteStopReason))
return null;
// must have active thread
if (!Debugger.Processes.Active.Threads.HaveActive)
return null;
// if event came from a different thread we finish tracing
var thread = Debugger.Processes.Active.Threads.Active;
Debug.Assert(thread != null);
if (thread.Id != threadId)
return null;
if (!thread.HaveCurrentFrame)
return null;
return thread.CurrentFrame;
}
...
}
}
还有两个方法需要解释:CalculateCallDepth
和 PrintCallStackTrace
。CalculateCallDepth
方法遍历线程的调用堆栈,通过计算当前帧与代表我们起始函数的帧(即我们开始跟踪的函数)之间的帧数来计算调用的深度。
namespace Mdb.Extension
{
[MDbgExtensionEntryPointClass(Url = "http://lowleveldesign.wordpress.com")]
public sealed class WatchTraceExtension : CommandBase
{
...
private static int CalculateCallDepth(MDbgFrame frame)
{
int depth = 0;
while (frame != null && frame.IsManaged)
{
Debug.Assert(frame.Function != null);
if (String.Equals(frame.Function.FullName,
startFuncName, StringComparison.Ordinal))
return depth;
frame = frame.NextUp;
depth++;
}
return -1;
}
...
}
}
PrintCallStackTrace
仅检查当前函数名是否符合用户提供的命名空间约束,并根据其调用深度以适当的缩进打印函数名。
namespace Mdb.Extension
{
[MDbgExtensionEntryPointClass
(Url = "http://lowleveldesign.wordpress.com")]
public sealed class WatchTraceExtension : CommandBase
{
...
private static void PrintCallStackTrace(String funcName, int depth)
{
if ((incRgx == null || incRgx.IsMatch(funcName)) &&
(excRgx == null || !excRgx.IsMatch(funcName)))
{
String indentStr = new String(' ', depth);
WriteOutput(String.Format("[{0,3}]
{1}{2}", depth, indentStr, funcName));
}
}
...
}
}
在 Mdbg 中使用我们的扩展
不幸的是,MDbg 不是平台无关的,如果您有一个 32 位应用程序,您需要一个 32 位 MDbg 版本才能成功调试它(对于 64 位,您需要一个 64 位 MDbg 版本)。幸运的是,这两个版本都应随 Windows SDK 一起安装在 c:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\NETFX 4.0 Tools\ 和 c:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\NETFX 4.0 Tools\x64\ 文件夹中。因此,为这两个平台编译您的扩展,并将 DLL 复制到相应的目录。现在,我们终于准备好使用我们的扩展了。我们将调试一个非常简单的应用程序来展示扩展的工作原理。这是代码:
using System;
public class T {
public T() {
String str = TestClass.Test4;
}
}
public static class TestClass
{
public static T Test1() {
return new T();
}
public static void Test2() {
Test1();
}
public static void Test3() {
Test2();
}
public static String Test4 {
get { return "test"; }
}
public static void Main(String[] args) {
Test3();
}
}
编译它
csc /debug+ /platform:x86 Program.cs
然后在 MDbg 调试器下运行它
mdbg Program.exe
您应该会看到类似以下内容:
MDbg (Managed debugger) v4.0.30319.1 (RTMRel.030319-0100) started.
Copyright (C) Microsoft Corporation. All rights reserved.
For information about commands type "help";
to exit program type "quit".
run Program.exe
STOP: Breakpoint Hit
27: public static void Main(String[] args) {
[p#:0, t#:0] mdbg>
现在加载扩展...
[p#:0, t#:0] mdbg> load wtrace
trying to load: .\wtrace.dll
wtrace extension loaded
...然后发出 wt
命令,排除 TestClass.Test3
方法
[p#:0, t#:0] mdbg> wt -exc TestClass.Test3
[ 2] TestClass.Test2
[ 3] TestClass.Test1
[ 4] T..ctor
[ 5] System.Object..ctor
[ 4] T..ctor
[ 5] TestClass.get_Test4
[ 4] T..ctor
[ 3] TestClass.Test1
[ 2] TestClass.Test2
[ 0] TestClass.Main
STOP: Process Exited
mdbg>
在输出中,您可以看到一个带有缩进的方法调用树,其中排除了对 TestClass.Test3
的调用。尽管这种情况是一个非常简单的例子,但您可以在更复杂的场景中尝试 mdbg
和 wt
命令,例如转储 ASP.NET 管道调用或检查 WCF 服务是如何被调用的。例如,要调试 IIS 上的 ASP.NET 管道,您将首先附加到 IIS 实例,然后在 HttpRuntime.System.Web.HttpRuntime.ProcessRequestNotificationPrivate
上设置一个断点,并等待断点命中。当调试器在您的断点处停止时,使用 wt -l 6
并检查正在调用哪些方法。
我希望我鼓励您使用 MDbg 调试器。也许您对不同的扩展有想法,或者您已经知道一个很棒的扩展?如果是这样,请留下评论,以便他人也能找到它。:) 像往常一样,源代码和二进制文件可在我的博客示例网站上找到。