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






4.96/5 (41投票s)
本文提出了一系列适用于原生开发使用 Visual Studio 进行调试的更多技巧。
我之前一篇关于 Visual Studio 调试技巧的文章受到了极大的关注。这鼓励我分享更多的调试技术。因此,您可以在下面找到一系列用于调试原生应用程序的其他有用技巧(延续上一篇文章的编号)。这些技巧适用于 Visual Studio 2005 或更高版本(并且至少其中一些也适用于更早的版本)。如果您阅读推荐的阅读材料,可以获得关于每种技术的更多信息。
技巧 11:数据断点
可以指示调试器在特定内存位置的数据发生更改时中断。但是,一次最多只能创建 4 个此类硬件数据断点。数据断点只能在调试期间添加,可以通过菜单(调试 > 新建断点 > 新数据断点)或“断点”窗口添加。
您可以使用内存地址或计算结果为地址的表达式。尽管您可以监视堆栈和堆上的值,但我认为此功能主要有助于查找堆上值何时被更改。这对于识别内存损坏非常有帮助。
在下面的示例中,改变的是指针的值,而不是它所指向的对象的。为了找出在哪里发生的,我设置了一个指向指针值存储的内存位置的断点,即 `&ptr`(请注意,这必须在指针初始化后进行)。当数据发生更改时,即有人修改了指针的值,调试器就会中断,我就可以找出是哪段代码负责了。
推荐阅读
技巧 12:重命名线程
当您调试多线程应用程序时,“线程”窗口会显示已创建的线程以及当前正在运行的线程。线程越多,就越难确定您正在查看哪个线程(尤其是在多个线程运行相同的线程过程,而您不确定当前执行的是哪个线程实例时)。
调试器允许您更改线程的名称。使用线程上的上下文菜单并重命名它。
也可以通过编程方式命名线程,但这有点棘手,并且必须在线程启动后进行,否则调试器会以其默认命名约定重新初始化它。定义并使用以下函数来重命名线程。
typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // must be 0x1000
LPCSTR szName; // pointer to name (in same addr space)
DWORD dwThreadID; // thread ID (-1 caller thread)
DWORD dwFlags; // reserved for future use, most be zero
} THREADNAME_INFO;
void SetThreadName(DWORD dwThreadID, LPCSTR szThreadName)
{
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = szThreadName;
info.dwThreadID = dwThreadID;
info.dwFlags = 0;
__try
{
RaiseException(0x406D1388, 0, sizeof(info)/sizeof(DWORD), (DWORD*)&info);
}
__except (EXCEPTION_CONTINUE_EXECUTION)
{
}
}
附加阅读
技巧 13:在特定线程上中断
多线程应用程序的另一个有用技术是将断点筛选到特定的线程、进程甚至计算机。这可以通过在断点上使用 `Filter` 命令来实现。
调试器允许您指定任何组合(`AND`、`OR`、`NOT`)的 `ThreadName`、`ThreadId`、`ProcessName`、`ProcessId` 和 `MachineName`。了解如何设置线程名称本身就可以使此筛选更加简单。
推荐阅读
技巧 14:(大致)测量执行时间
在我之前的文章中,我写了关于“监视”窗口中的伪变量。当时没有提到的是 `@clk`,它显示一个计数器的值,可以帮助我们粗略了解两个断点之间的代码执行了多长时间。该值以微秒为单位。但是,这绝不是一种分析执行的方法。您应该为此使用 Visual Studio 分析器和/或性能计时器。
可以通过在“监视”窗口中添加 `@clk=0` 或在“立即”窗口中执行该命令来重置时钟。因此,要了解某段代码执行了多长时间,请执行以下操作:
- 在代码块的开头设置断点
- 在代码块的结尾设置断点
- 在“监视”窗口中添加 `@clk`
- 当第一个断点命中时,在“立即”窗口中键入 `@clk=0`。
- 运行程序,直到命中代码块末尾的断点,然后在“监视”窗口中检查 `@clk` 的值。
请注意,网上有一些技巧指示您在“监视”窗口中添加两个表达式:`@clk` 后跟 `@clk=0`,据称这将在每次命中断点时重置时钟。这在早期版本的 Visual Studio 中有效,但在(肯定在 VS2005 及更高版本中)不再有效。
附加阅读
技巧 15:格式化数字
当您在“监视”或“快速监视”窗口中监视变量时,值会使用预定义的默认可视化工具显示。对于数字,它们将根据其类型(`integer`、`float`、`double`)和十进制基数显示。但是,您可以强制调试器以不同的类型、不同的数字基数或同时两者显示数字。
要更改显示的类型,请在变量前加上
- `by` 表示无符号字符(又名无符号字节)
- `wo` 表示无符号短整型(又名无符号字)
- `dw` 表示无符号长整型(又名无符号双字)
要更改显示的基数,请在变量名后加上
- `,d` 或 `,i` 表示带符号十进制
- `,u` 表示无符号十进制
- `,o` 表示无符号八进制
- `,x` 表示小写十六进制或 `,X` 表示大写十六进制
附加阅读
技巧 16:格式化(内存)数据
除了数字之外,调试器还可以在“监视”窗口中显示格式化的内存值,最多 64 字节。您可以使用以下任一说明符跟在表达式(变量或内存地址)后面以格式化数据
- `mb` 或 `m` - 16 字节十六进制,后跟 16 个 ASCII 字符
- `mw` - 8 个字
- `md` - 4 个双字
- `mq` - 2 个四字
- `ma` - 64 个 ASCII 字符
- `mu` - 2 字节的 UNICODE 字符
推荐阅读
技巧 17:在系统 DLL 上中断
有时,在调用 DLL(如系统 DLL,例如 *kernel32.dll* 或 *user32.dll*)中的某个函数时中断很有用。为此,必须使用原生调试器提供的上下文运算符。您可以限定断点位置、变量名或表达式
- {[函数],[源],[模块]} 位置
- {[函数],[源],[模块]} 变量名
- {[函数],[源],[模块]} 表达式
花括号可以包含任何组合的函数名、源和模块,但逗号不得省略。
假设我们想在调用 `CreateThread` 时中断。此函数是从 *kernel32.dll* 导出的,因此上下文运算符应如下所示:`{,,kernel32.dll}CreateThread`。但是,这不起作用,因为运算符需要 `CreateThread` 的装饰名称。您可以使用 DBH.exe 来找出特定函数的装饰名称。
以下是如何找出 `CreateThread` 的装饰名称
C:\Program Files (x86)\Debugging Tools for Windows (x86)>dbh.exe -s:srv*C:\Symbo
ls*http://msdl.microsoft.com/Download/Symbols -d C:\Windows\SysWOW64\kernel32.dl
l enum *CreateThread*
Symbol Search Path: srv*C:\Symbols*http://msdl.microsoft.com/Download/Symbols
index address name
1 10b4f65 : _BaseCreateThreadPoolThread@12
2 102e6b7 : _CreateThreadpoolWork@12
3 103234c : _CreateThreadpoolStub@4
4 1011ea8 : _CreateThreadStub@24
5 1019d40 : _NtWow64CsrBasepCreateThread@12
6 1019464 : ??_C@_0BC@PKLIFPAJ@SHCreateThreadRef?$AA@
7 107309c : ??_C@_0BD@CIEDBPNA@TF_CreateThreadMgr?$AA@
8 102ce87 : _CreateThreadpoolCleanupGroupStub@0
9 1038fe3 : _CreateThreadpoolIoStub@16
a 102e6f0 : _CreateThreadpoolTimer@12
b 102e759 : _CreateThreadpoolWaitStub@12
c 102ce8e : _CreateThreadpoolCleanupGroup@0
d 102e6e3 : _CreateThreadpoolTimerStub@12
e 1038ff0 : _CreateThreadpoolIo@16
f 102e766 : _CreateThreadpoolWait@12
10 102e6aa : _CreateThreadpoolWorkStub@12
11 1032359 : _CreateThreadpool@4
看起来实际名称是 `_CreateThreadStub@24`。因此,我们应该在 `{,,kernel32.dll}_CreateThreadStub@24` 处创建一个断点。
运行程序,当它中断时,忽略没有与断点关联的源代码的消息。
使用“调用堆栈”窗口导航到调用该函数的代码。
推荐阅读
技巧 18:加载符号
当您调试应用程序时,“调用堆栈”窗口可能无法显示完整的调用堆栈,而是跳过有关系统 DLL(如 *kernel32.dll* 和 *user32.dll*)的信息。
可以通过加载这些 DLL 的符号来获得完整的堆栈。这可以直接从“调用堆栈”窗口使用上下文菜单完成。您可以从预指定的符号路径下载,或者从 Microsoft 的符号服务器下载(如果是系统 DLL)。下载并加载符号到调试器后,“调用堆栈”会更新。
也可以从“模块”窗口加载符号。
下载后,符号将存储在缓存中,可以从“工具”>“选项”>“调试”>“符号”进行配置。
技巧 19:MFC 中的内存泄漏报告
如果您想在 MFC 应用程序中获取内存泄漏报告,可以重新定义 `new` 运算符为一个 `DEBUG_NEW` 宏,这是 `new` 运算符的一个修改版本,它跟踪每个分配对象的文件名和行号。在发布版本中,`DEBUG_NEW` 解析为 `operator new`。
向导生成的 MFC 源文件在 `#includes` 之后包含以下预处理器指令
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
这就是如何重新定义 `new` 运算符。
但是,许多 STL 头文件与此版本的 `operator new` 不兼容。如果您在重新定义 `new` 和 `DEBUG_NEW` 之后包含 `
1>c:\program files (x86)\microsoft visual studio 9.0\vc\include\xmemory(43) :
error C2665: 'operator new' : none of the 5 overloads could convert all the argument types
1> c:\program files\microsoft visual studio 9.0\vc\include\new.h(85):
could be 'void *operator new(size_t,const std::nothrow_t &) throw()'
1> c:\program files\microsoft visual studio 9.0\vc\include\new.h(93): or
'void *operator new(size_t,void *)'
1> while trying to match the argument list '(const char [70], int)'
1> c:\program files (x86)\microsoft visual studio 9.0\vc\include\xmemory(145) :
see reference to function template instantiation '_Ty *std::_Allocate<char>(size_t,_Ty *)'
being compiled
1> with
1> [
1> _Ty=char
1> ]
1> c:\program files (x86)\microsoft visual studio 9.0\vc\include\xmemory(144) :
while compiling class template member function 'char *std::allocator<_Ty>::allocate
(std::allocator<_Ty>::size_type)'
1> with
1> [
1> _Ty=char
1> ]
1> c:\program files (x86)\microsoft visual studio 9.0\vc\include\xstring(2216) :
see reference to class template instantiation 'std::allocator<_Ty>' being compiled
1> with
1> [
1> _Ty=char
1> ]
解决方案是始终在用 `DEBUG_NEW` 重新定义 `new` 之前包含这些 STL 头文件。
附加阅读
技巧 20:调试 ATL
当您开发 ATL COM 组件时,调试器可以帮助您监视 COM 对象上的 `QueryInterface`、`AddRef` 和 `Release` 调用。此支持默认未启用,但您可以将两个宏指定到预处理器定义或预编译头文件。当定义了这些宏时,关于这些调用的信息将显示在“输出”窗口中。
这两个宏是
- `_ATL_DEBUG_QI` 显示查询您对象上每个接口的名称。它必须在包含 *atlcom.h* 之前定义。
- `_ATL_DEBUG_INTERFACES` 在每次调用 `AddRef` 或 `Release` 时显示接口的当前引用计数以及类名和接口名称。它必须在包含 *atlbase.h* 之前定义。
推荐阅读
结论
本文和上一篇文章中提供的技巧(尽管它们并未涵盖关于调试的所有内容)应该能让您为原生应用程序可能遇到的绝大多数调试情况做好准备。