Visual Studio 中的高级调试






4.92/5 (198投票s)
学习 Visual Studio 中高级调试的技巧和窍门。
引言
我们许多开发人员在 Visual Studio 中调试时,除了基本的 F9、F10、F11、F5 和 Watch 窗口之外,很少使用其他功能。因此,我们最终浪费数小时调试一个问题或模拟一个条件,而如果利用 Visual Studio 中开箱即用的丰富调试功能,这些问题本来可以在几分钟内解决。
高级调试技巧散布在网络各处,但我认为一个整合的列表对于开发人员掌握和开始使用这些技术会非常有帮助。
环境
本文中的技巧应该适用于 Visual Studio 2008/2010。其中许多技巧可能在下一个 Visual Studio 版本中仍然有效。
技巧列表
为了更容易地阅读本文,我将其分为六个不同的技巧,我将通过示例代码和屏幕截图进行展示。
- “创建对象 ID”的魔力
- 附加到进程 - 使用宏
- 即时窗口
- 直接调用函数
- 设置和显示变量
- 调试 Windows 服务
- 玩转断点
- 跟踪点
- 条件
- 命中次数
- Filter
- 更改断点位置
- 局部变量/自动窗口/调用堆栈
额外提示! 断点命中时启用声音
1.“创建对象 ID”的魔力
有时,我们希望即使对象超出范围后也能跟踪它。我们可能需要这种能力来调试一个需要我们跟踪对象直到它被垃圾回收的问题。Visual Studio 调试器中的对象 ID 功能提供了这种能力。请按照以下步骤亲自尝试。
- 在代码中使用您想要跟踪的变量的行上设置一个断点,如下所示
- 在调试模式下运行您的应用程序,并让它在断点处停止。
- 右键单击
str
并单击“添加监视”。 - 在您的“监视 1”窗口中,右键单击对象变量
str
,然后从上下文菜单中选择“创建对象 ID”。 - 您现在将在“值”列中看到
1#
。这是调试器为当前调试会话中您的变量提供的唯一 ID。 - 即使
str
超出范围,我们也可以使用此 ID 跟踪对象值,如下所示。只需在监视窗口中放入对象 ID1#
即可监视其值。 - 如果继续
for
循环的迭代,str
的值会改变,但1#
保持不变。这告诉我们,尽管之前的str
对象已超出范围,我们仍然可以使用我们分配给它的对象 ID 来跟踪其值。 - 最后,如果您离开函数,那么
str
的所有实例都将超出范围,您将无法再使用“监视”窗口跟踪str
。它会变灰。但是,对象 ID1#
仍然处于活动状态,您可以在遍历其他函数时继续跟踪其值。
注意:顾名思义,这仅适用于引用类型,而不适用于值类型。这是有道理的,因为值类型将存储在堆栈上,并在作用域结束时立即弹出。因此,它们通常不依赖于垃圾回收器进行清理。
2. 附加到进程 – 使用宏
我们在 Visual Studio 中执行的许多任务都是重复性的,可以使用宏自动化。一个这样的例子是附加到进程进行调试。能够调试现有运行进程(例如:.NET 控制台应用程序 EXE 的进程)是一个常见的需求。通常的方法是使用 Visual Studio 中调试 -> 附加到进程窗口。但是,如果我们不得不反复执行此操作以测试迭代更改,这可能会变得繁琐和令人恼火。这时宏就派上用场了。
- 创建一个简单的控制台应用程序,包含一个
Main
方法和一个TestAttachToProcessMacro
方法,如下所示。从Main
函数调用此方法。 - 构建控制台应用程序。这将在 debug 文件夹中生成 EXE 文件。双击并使用此 EXE 启动应用程序。
- 上面代码中显示的第一个断点将不会被命中(因为我们尚未进行调试),您将在控制台窗口中看到以下输出。
- 我们希望通过附加到此进程从第二个断点开始调试,所以现在我们通过 5 个简单步骤开始录制宏
- 从工具 -> 宏菜单中单击录制临时宏,如下所示
- 录制已开始。现在执行必要的动作以附加到进程,如下所示
点击调试 -> 附加到进程
在下面的弹出窗口中,找到您的进程并单击“附加”。
- 使用工具 -> 宏停止录制宏,如下所示
- 使用工具 -> 宏保存宏,如下所示
- 保存后,宏将出现在“宏资源管理器”中。我将其命名为
AttachToMyProgram
。
- 从工具 -> 宏菜单中单击录制临时宏,如下所示
- 最后,我们还可以在调试工具栏上放置此宏的快捷方式,使事情变得更简单。
- 转到工具 -> 自定义 -> 命令,然后在工具栏下拉列表中选择调试,如下所示
- 点击“添加命令”按钮,在下面的弹出窗口中,在类别下选择
宏
,在命令下选择AttachToMyProgram
- 现在从“修改选择”下方,重命名命令,如下所示
- 现在,
AttachToMyProgram
快捷方式会出现在调试工具栏中,如下所示
- 转到工具 -> 自定义 -> 命令,然后在工具栏下拉列表中选择调试,如下所示
- 现在关闭控制台应用程序并重新启动。我们将再次看到“我已启动”消息。现在只需点击调试栏上的
AttachToMyProcess
快捷方式,并在控制台应用程序窗口中按任意键。瞧!您已进入调试会话,第二个断点已命中。现在,您只需单击一个按钮即可轻松附加到您的进程。
3. 即时窗口
很多时候,我们编写了一个函数,并希望直接反复调试该函数,直到它给出我们需要的输出。我们许多人每次调试时都运行整个应用程序,只为了到达那个函数。嗯,那是不必要的。这时即时窗口就派上用场了。您可以使用键盘快捷键 Ctrl + Alt + I 打开它。
它是这样工作的
直接调用函数
让我们尝试直接从即时窗口调用下面的函数
我们可以直接从即时窗口调用此函数,如下所示
在即时窗口中按 Enter 键后,TestImmediateWindow1()
函数中的断点将被命中,而无需您调试整个应用程序。
继续执行,您也会在即时窗口中看到输出,如下所示
您可以更改 _test
变量的值并测试反向输出
设置和显示变量
我们可能希望将变量传递给我们从即时窗口调用的函数。让我们看一个下面的函数示例
使用即时窗口中的命令,如下所示,我们可以声明、设置变量并将其传递给我们的函数。
以下是另一个示例,用于调用一个函数,传递一个复杂的对象类型,例如 Employee
类的对象。
用于测试函数的即时窗口命令
您可以使用即时窗口做更多事情,但如果您感兴趣,我将留给您自己探索。
4. 调试 Windows 服务
如果您不了解此技巧,调试 Windows 服务可能会成为一项艰巨的任务。您会构建并部署服务并启动它。然后从 Visual Studio 中,您将使用“附加到进程”开始调试。即使那样,如果您需要调试 OnStart
方法中发生的情况,那么您将不得不进行 Thread.Sleep()
或其他操作,以便在您附加到进程时 OnStart
方法会等待您。通过这个简单的技巧,我们可以避免所有的麻烦。
步骤 1:将 Windows 服务的输出类型设置为控制台应用程序
步骤 2:接下来,删除 Program.cs 文件,而是将以下代码粘贴到继承自 ServiceBase
的服务文件中。就这样。现在您可以在调试中运行 Windows 服务,它将作为控制台应用程序运行。或者您可以像往常一样部署,它将作为 Windows 服务运行。
partial class MyService : ServiceBase
{
public static void Main(string[] args)
{
/*EDIT: 18th January 2012
* As per suggestion from Blaise in his comments I have added the
Debugger.Launch condition so that you
* can attach a debugger to the published service when it is about to start.
* Note: Remember to either remove this code before you release to production or
* do not release to production only in the 'Release' configuration.
* Ref: http://weblogs.asp.net/paulballard/archive/2005/07/12/419175.aspx
*/
#if DEBUG
System.Diagnostics.Debugger.Launch();
#endif
/*EDIT: 18 January 2012
Below is Psuedo code for an alternative way
suggested by RudolfHenning in his comment. However, I find
Debugger.Launch() a better option.
#if DEBUG
//The following code is simply to ease
//attaching the debugger to the service to debug the startup routine
DateTime startTime = DateTime.Now;
// Waiting until debugger is attached
while ((!Debugger.IsAttached) &&
((TimeSpan)DateTime.Now.Subtract(startTime)).TotalSeconds < 20)
{
RequestAdditionalTime(1000); // Prevents the service from timeout
Thread.Sleep(1000); // Gives you time to attach the debugger
}
// increase as needed to prevent timeouts
RequestAdditionalTime(5000); // for Debugging
// the OnStart method <- set breakpoint here,
#endif
*/
var service = new MyService();
/* The flag Environment.UserInteractive is the key here.
* If its true means the app is running
* in debug mode. So manually call the functions OnStart() and
* OnStop() else use the ServiceBase
* class to handle it.*/
if (Environment.UserInteractive)
{
service.OnStart(args);
Console.WriteLine("Press any key to stop the service..");
Console.Read();
service.OnStop();
}
else
{
ServiceBase.Run(service);
}
}
public MyService()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
}
protected override void OnStop()
{
}
}
5. 玩转断点
您可以单独使用以下断点变体,也可以将它们组合起来,享受这杯鸡尾酒!
跟踪点(命中时..)
有时,我们希望每次执行特定代码行时都观察一个或多个变量的值。通过设置普通断点来做到这一点可能会非常耗时。所以我们通常使用 Console.WriteLine
来打印值。相反,如果是临时检查,使用 TracePoints 更好。它的作用与 Console.WriteLine
相同。优点是您不必通过添加 Console.WriteLine
来干扰代码,并且可以避免完成任务后忘记删除它的风险。更好的是,通过这种方式,您可以通过在 TracePoint
上叠加不同的断点条件来利用断点的其他功能。
让我们看看跟踪点的实际应用。
在调用 ReverseString
函数处设置一个断点,如下所示
然后右键单击并点击“命中时..”,然后勾选“打印消息”。在测试框中,复制“Value of reverseMe = {reverseMe}
”。保持“继续执行”被勾选,然后点击“确定”。
断点将转换为 TracePoint
(菱形),如下所示
现在,每当断点被命中时,它不会中断代码,而是继续执行,您将在输出窗口中看到 reverseMe
变量每次命中的值,如下所示
条件
如果只希望断点在特定值下命中,可以使用条件断点来避免在代码中编写额外的 if
/else
条件。
右键单击我们上面设置的跟踪点,然后从“断点”下点击“条件”。然后在条件文本框中输入“i==45
”并点击“确定”。(重要:在条件中切勿使用单个“=
”。始终使用“==
”。)
现在断点只会在 i = 45;
时激活;所以跟踪点应该只打印“Live45
”。
命中次数
命中次数可用于查找断点被命中的次数。此外,您可以选择何时在断点处中断。将条件断点更改为 i > 45
。然后右键单击 -> 断点 -> 命中次数。选择“当命中次数为..的倍数时中断”并输入 5 作为值。单击“确定”。
现在,断点将在每 5 次迭代后命中。请注意,下面的输出是条件断点和命中次数断点的共同作用。
下面显示的命中次数表示断点在 i = 46
到 i = 99
之间被命中了 54 次,但它只在每 5 次迭代后中断执行。
Filter
对于多线程应用程序很有用。如果多个线程调用同一个函数,您可以使用过滤器指定断点应该在哪个线程上命中。
右键单击 -> 断点 -> 筛选器
更改断点位置
如果您想将断点移动到不同的行,请使用此选项
6. 局部变量/自动窗口/调用堆栈
在调试时,以下三个窗口可能会派上用场。您可以在开始调试后访问它们。在 Visual Studio 菜单栏中,转到调试 -> 窗口。
自动窗口 (AUTOS):自动窗口显示当前语句和前一个语句中使用的变量。帮助您只关注当前行及其周围使用的变量。
(对于 Visual Basic.Net,它显示当前语句和当前语句两侧三个语句中的变量。)
局部变量 (LOCALS):局部变量窗口显示当前上下文的局部变量。您可以在此处观察函数中局部变量的值。请注意,类级别变量在局部变量下将不可见。
调用堆栈 (CALL STACK):调用堆栈显示导致当前函数调用的整个函数调用树。可以帮助您追溯罪魁祸首!
额外提示! 断点命中时启用声音,
- 转到控制面板 -> 声音和音频设备(Windows XP)。在 Windows 7 中是控制面板 -> 声音。
- 在“声音”选项卡下的“程序事件”中找到并选择“断点命中”(参见下图)
- 选择您喜欢的声音并点击“确定”。
- 现在,当断点被命中时,您将听到声音!
历史
- 2012 年 1 月 18 日:采纳了 Blaise 的建议,在技巧 4 中添加了
Debugger.Launch()
选项 - 2012 年 1 月 23 日:根据 Shivprasad Koirala 的建议,在技巧 1 - 创建对象 ID 的末尾添加了一个注释。