10 个原生开发 Visual Studio 调试技巧(又十个)






4.98/5 (107投票s)
本文提出了一个关于使用 Visual Studio 进行原生开发的调试技巧列表。
我最近偶然看到了 Ivan Shcherbakov 的这篇文章,名为《10+ 个强大的 Visual Studio 调试技巧》。尽管这篇文章介绍了一些相当基础的 Visual Studio 调试技巧,但还有其他同样有用的技巧。因此,我整理了一个关于原生开发的其他十个调试技巧列表,这些技巧至少适用于 Visual Studio 2008。(如果您处理的是托管代码,调试器会有更多功能,CodeProject 上也有几篇介绍这些功能的文章。)以下是我补充的技巧列表:
更多调试技巧,请查看本系列的第二篇文章:《10 个更多 Visual Studio 原生开发调试技巧》。
技巧 1:异常时中断
可以指示调试器在异常发生时,在调用处理程序之前中断。这允许您在异常发生后立即调试应用程序。通过导航调用堆栈,应该可以找出异常的根本原因。
Visual Studio 允许您指定要中断的异常类别或特定异常。可以在“调试”>“异常”菜单中找到一个对话框。您可以指定原生(或托管)异常,除了调试器已知的默认异常之外,还可以添加自定义异常。
以下是一个在抛出 std::exception
时调试器中断的示例。
其他阅读材料
技巧 2:监视窗口中的伪变量
监视窗口或“快速监视”对话框支持一些特殊的(调试器识别的)变量,称为伪变量。已文档化的包括:
$tid
– 当前线程的线程 ID$pid
– 进程 ID$cmdline
– 启动程序的命令行字符串$user
– 运行程序的帐户信息$registername
– 显示寄存器 registername 的内容
然而,一个非常实用的伪变量是用于最后错误的代码:
$err
– 显示最后错误的数字代码$err
,hr
– 显示最后错误的描述信息
附加阅读
技巧 3:符号超出作用域后监视堆对象
有时,您希望在对象(在堆上)的符号超出作用域后仍然监视其值。当发生这种情况时,监视窗口中的变量将被禁用,无法再检查(也无法更新),即使对象仍然存在。如果您知道对象的地址,就可以继续完全监视它。然后,您可以将地址强制转换为该对象类型的指针,并将其放入监视窗口。
在下面的示例中,在退出 do_foo()
后,_foo
不再可在监视窗口中访问。但是,获取其地址并将其强制转换为 foo*
,我们仍然可以监视该对象。
技巧 4:监视数组中的值范围
如果您处理大型数组(假设至少有几百个元素,但可能更少),在监视窗口中展开数组并查找特定范围的元素会很麻烦,因为您需要滚动很多次。如果数组在堆上分配,您甚至无法在监视窗口中展开其元素。对此有一个解决方案。您可以使用语法 (array + <offset>), <count>
来监视从 <offset>
位置开始的 <count>
个元素的特定范围(当然,这里的 array 是您的实际对象)。如果您想监视整个数组,只需说 array, <count>
即可。
如果您的数组在堆上,您可以在监视窗口中展开它,但要监视特定范围,您需要使用略有不同的语法:((T*)array + <offset>), <count>
(请注意,此语法也适用于堆上的数组)。在这种情况下,T
是数组元素的类型。
如果您使用 MFC 并使用其中的“数组”容器,例如 CArray
、CDWordArray
、CStringArray
等,您可以应用相同的过滤,只不过您必须监视数组的 m_pData
成员,该成员是实际保存数据的缓冲区。
技巧 5:避免单步进入不希望的函数
在调试代码时,您可能经常单步进入那些您希望跳过的函数,无论是构造函数、赋值运算符还是其他函数。曾经最困扰我的是 CString
构造函数。这是一个示例,在单步进入 take_a_string()
函数时,会先单步进入 CString
的构造函数。
void take_a_string(CString const &text)
{
}
void test_string()
{
take_a_string(_T("sample"));
}
幸运的是,您可以指示调试器跳过某些方法、类或整个命名空间。实现此功能的方式已发生变化。在 VS 6 时代,这通过 autoexp.dat 文件指定。自 Visual Studio 2002 起,已更改为注册表设置。要启用跳过函数,您需要在注册表中添加一些值(您可以在 这里找到所有详细信息)。
- 实际位置取决于您的 Visual Studio 版本和操作系统平台(x86 或 x64,因为注册表对 64 位 Windows 有两个视图)。
- 值名称是一个数字,表示规则的优先级;数字越大,规则的优先级越高。
- 值数据是一个
REG_SZ
值,表示一个正则表达式,用于指定要过滤什么以及执行什么操作。
为了跳过任何 CString
方法,我添加了以下规则:
启用此设置后,即使您在上面的示例中按“单步进入”进入 take_a_string()
,调试器也会跳过 CString
的构造函数。
其他阅读材料
技巧 6:从代码启动调试器
有时,您可能需要附加调试器到程序,但无法通过“附加”窗口做到这一点(也许是因为中断发生得太快而无法附加),也无法在开始时就用调试器启动程序。您可以通过调用 __debugbreak()
内建函数来导致程序中断,并让调试器有机会附加。
void break_for_debugging()
{
__debugbreak();
}
实际上还有其他方法可以做到这一点,例如触发中断 3,但这只适用于 x86 平台(C++ 不再支持 x64 的汇编)。还有一个 DebugBreak() 函数,但它不可移植,因此内建函数是推荐的方法。
__asm int 3;
当您的程序执行内建函数时,它会停止,您就有机会将调试器附加到进程。
其他阅读材料
技巧 7:输出到输出窗口
通过调用 DebugOutputString
可以将特定文本显示在调试器的输出窗口中。如果没有附加调试器,该函数将不执行任何操作。
技巧 8:内存泄漏隔离
内存泄漏是原生开发中的一个重要问题,在大型项目中查找它们可能非常具有挑战性。Visual Studio 提供有关检测到的内存泄漏的报告,还有其他应用程序(免费或商业)可以帮助您。然而,在某些情况下,可以使用调试器在发生最终会导致泄漏的分配时进行中断。要做到这一点,您必须找到一个可重现的分配编号(但这可能并不容易)。如果您能够做到这一点,调试器就可以在执行分配时立即中断。
让我们考虑这段分配了 8 字节但从未释放已分配内存的代码。Visual Studio 会显示泄漏对象的报告,多次运行此程序后,我发现分配编号(341
)始终是相同的。
void leak_some_memory()
{
char* buffer = new char[8];
}
Dumping objects ->
d:\marius\vc++\debuggingdemos\debuggingdemos.cpp(103) : {341} normal block at 0x00F71F38, 8 bytes long.
Data: < > CD CD CD CD CD CD CD CD
Object dump complete.
在特定(可重现)分配时中断的步骤是:
- 确保您有适当的内存泄漏报告模式(请参阅 使用 CRT 库查找内存泄漏)。
- 多次运行程序,直到在程序结束时的内存泄漏报告中找到可重现的分配编号(例如我上面的示例中的 {341})。
- 在程序开始时设置一个断点,以便您尽可能早地中断。
- 用调试器启动应用程序。
- 当命中初始断点时,在监视窗口的“名称”列中输入:
{,,msvcr90d.dll}_crtBreakAlloc
,然后在“值”列中输入您要调查的分配编号。 - 继续调试(F5)。
- 执行将在指定分配处停止。您可以使用调用堆栈导航回触发分配的代码。
按照这些步骤处理我的示例中的分配编号 341
,我能够确定泄漏的来源。
技巧 9:调试 Release 版本
Debug 和 Release 版本有不同的用途。Debug 配置用于开发,而 Release 配置顾名思义,应该用于程序的最终版本。由于应用程序被认为是满足发布所需质量的,因此这种配置包含会破坏 Debug 版本调试体验的优化和设置。然而,有时您希望能够像调试 Debug 版本一样调试 Release 版本。要做到这一点,您需要对配置进行一些更改。但是,在这种情况下,有人可能会争辩说您不再调试 Release 版本,而是调试 Debug 和 Release 版本的混合体。
有几件事您应该做;强制性的包括:
- C/C++ > 常规 > 调试信息格式应为“程序数据库 (
/Zi
)” - C/C++ > 优化 > 优化应为“禁用 (
/Od
)” - 链接器 > 调试 > 生成调试信息应为“是 (
/DEBUG
)”
附加阅读
技巧 10:远程调试
另一个重要的调试体验是远程调试。这是一个较大的主题,已经被讨论过很多次,所以我只想做一点总结。
- 您需要在远程计算机上安装远程调试监视器。
- 远程调试监视器必须“以管理员身份”运行,并且用户必须是管理员组的成员。
- 当您运行监视器时,它会启动一个新服务器,您必须在 Visual Studio 的“附加到进程”窗口的“限定符”组合框中使用该服务器的名称。
- 远程计算机和本地计算机上的防火墙必须允许 Visual Studio 和远程调试监视器之间的通信。
- 要能够调试,PDB 文件至关重要;为了让 Visual Studio 调试器能够自动加载它们:
- 原生 PDB 文件必须可在本地计算机上(在与远程计算机上的相应模块相同的路径上)。
- 托管 PDB 文件必须可在远程计算机上。
远程调试监视器下载
其他阅读材料
结论
本文介绍的调试技巧和启发本文的 原始文章 应该为大多数调试经验和问题提供必要的技巧。要获取有关这些技巧的更多信息,我建议阅读其他阅读材料。