65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (41投票s)

2012年12月29日

CPOL

8分钟阅读

viewsIcon

78974

本文提出了一系列适用于原生开发使用 Visual Studio 进行调试的更多技巧。

我之前一篇关于 Visual Studio 调试技巧的文章受到了极大的关注。这鼓励我分享更多的调试技术。因此,您可以在下面找到一系列用于调试原生应用程序的其他有用技巧(延续上一篇文章的编号)。这些技巧适用于 Visual Studio 2005 或更高版本(并且至少其中一些也适用于更早的版本)。如果您阅读推荐的阅读材料,可以获得关于每种技术的更多信息。

  1. 数据断点
  2. 重命名线程
  3. 在特定线程上中断
  4. (大致) 测量执行时间
  5. 格式化数字
  6. 格式化(内存)数据
  7. 在系统 DLL 上中断
  8. 加载符号
  9. MFC 中的内存泄漏报告
  10. 调试 ATL

技巧 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* 之前定义。

推荐阅读

结论

本文和上一篇文章中提供的技巧(尽管它们并未涵盖关于调试的所有内容)应该能让您为原生应用程序可能遇到的绝大多数调试情况做好准备。

© . All rights reserved.