Heim >Backend-Entwicklung >C#.Net-Tutorial >Teilen Sie 10 Debugging-Kenntnisse für die native Entwicklung von Visual Studio (2)
Mein vorheriger Artikel über Visual Studio-Debugging-Fähigkeiten hat so großes Interesse geweckt, dass ich beschlossen habe, mehr Debugging-Wissen zu teilen. In der folgenden Liste finden Sie Debugging-Tipps zum Schreiben nativer Entwicklung (nummeriert nach dem vorherigen Artikel). Diese Tipps können auf VS2005 oder neuere Versionen angewendet werden (einige können natürlich auch auf ältere Versionen angewendet werden). Wenn Sie fortfahren, können Sie die Details der einzelnen Tipps erfahren.
Tipp 11: Datenhaltepunkte
Wenn sich der Speicherort der Daten ändert, bricht der Debugger ab. Allerdings ist es mit dieser Hardware nur möglich, 4 Daten-Breakpoints gleichzeitig zu erstellen. Datenhaltepunkte können nur während des Kompilierungsprozesses hinzugefügt werden und können über das Menü (Kompilieren > Neuer Haltepunkt > Neuer Datenhaltepunkt) oder über das Haltepunktfenster hinzugefügt werden.
Sie können eine Speicheradresse oder einen Adressausdruck verwenden. Obwohl Sie beide Werte auf dem Stapel sehen können, ist diese Funktion meiner Meinung nach normalerweise nur dann nützlich, wenn der Wert auf dem Heap geändert wurde. Dies ist eine große Hilfe bei der Identifizierung von Speicherbeschädigungen.
Im folgenden Beispiel wurde der Wert des Zeigers in den Wert des gezeigten Objekts geändert. Um herauszufinden, wo die Änderungen vorgenommen wurden, setze ich einen Haltepunkt, an dem der Zeigerwert gespeichert wird, z. B. &ptr (beachten Sie, dass dies geschieht, nachdem der Zeiger initialisiert wurde). Wenn sich die Daten ändern, vorausgesetzt, jemand hat den Wert des Zeigers geändert, wird der Debugger beendet und kann herausfinden, welcher Code die Änderung verursacht hat.
Tipp 12: Thread-Umbenennung
Wenn Sie eine Multithread-Anwendung debuggen, wird im Threads-Fenster angezeigt, welche Threads erstellt wurden und welcher aktuelle Thread. Je mehr Threads vorhanden sind, desto schwieriger ist es für Sie, den gesuchten Thread zu finden (insbesondere wenn dasselbe Programm von mehreren Threads gleichzeitig ausgeführt wird, wissen Sie nicht, welche Thread-Instanz gerade ausgeführt wird)
Mit dem Debugger können Sie Threads umbenennen. Klicken Sie mit der rechten Maustaste auf einen Thread und benennen Sie ihn um.
Es ist auch möglich, Threads programmgesteuert zu benennen, allerdings ist dies etwas knifflig und muss nach dem Start des Threads erfolgen, da der Debugger ihn sonst mit seiner Standardeinstellung umbenennt Namenskonvention: Die folgende Funktion zeigt, wie ein Thread definiert und verwendet wird.
typedef struct tagTHREADNAME_INFO { DWORD dwType; // 必须是两个字节的长度 LPCSTR szName; // 指针指向命名 (同一个地址空间) DWORD dwThreadID; // 线程ID(-1调用线程) DWORD dwFlags; // 保留待用,多数情况下为0 } 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) { } }
Tipp 13: Haltepunkte für bestimmte Threads festlegen
Ein weiterer nützlicher Tipp für Multithread-Anwendungen ist das Setzen von Haltepunkten für bestimmte Threads, Prozesse oder sogar Computer. Sie können den Befehl breakpoint Filer verwenden, um diese Funktion zu erreichen.
Der Debugger ermöglicht Ihnen die Verwendung verschiedener Kombinationen aus Thread-Name, Thread-ID, Prozessname, Prozess-ID und Maschinenname (mittels AND-, OR-, NOT-Verbindungen). Wenn Sie wissen, wie Thread-Namen festgelegt werden, ist diese Filtertechnik auch einfacher zu bedienen.
Tipp 14: (Ungenaue) Timing-Ausführung
In meinem vorherigen Artikel habe ich die Verwendung von Dummy-Variablen im Überwachungsfenster erwähnt. Einer, der nicht erwähnt wird, ist @clk, der den Wert eines Zählers anzeigen kann, um die ungefähre Zeit zu ermitteln, die für die Codeausführung zwischen zwei Haltepunkten erforderlich ist. Die Einheit dieses Werts ist Millisekunden (ms). Diese Methode kann jedoch nicht zur Konfiguration der Programmausführung verwendet werden. Sie sollten Visual Studio Profiler oder Performance Timer verwenden, um diese Konfigurationen abzuschließen.
Setzen Sie den Timer zurück, indem Sie @clk=0 im Überwachungsfenster oder Direktfenster hinzufügen. Wenn Sie daher die Zeit berechnen müssen, die zum Ausführen des letzten Codeabschnitts erforderlich ist, gehen Sie wie folgt vor:
Setzen Sie einen Haltepunkt am Anfang des Codeblocks.
Setzen Sie einen Haltepunkt am Ende des Codeblocks
Fügen Sie @clk im Überwachungsfenster hinzu
Wenn der erste Haltepunkt ausgelöst wird, geben Sie @clk=0 im Zwischenfenster ein
Führen Sie das Programm aus, bis Am Ende des Codeblocks wird ein Haltepunkt an der Position gefunden und der Wert von @clk im Überwachungsfenster angezeigt.
Beachten Sie, dass es im Internet Tipps gibt, dass Sie zwei Ausdrücke im Überwachungsfenster hinzufügen müssen: @clk und @clk=0. Es wird gesagt, dass Sie es jedes Mal hinzufügen können. Der Timer wird an dem Punkt zurückgesetzt, an dem der Haltepunkt ausgeführt wird. Diese Technik kann nur in niedrigeren Versionen von Visual Studio verwendet werden, nicht jedoch in höheren Versionen von VS, wie z. B. VS2005 (der Autor hat es getestet, und vs2005 unterstützt diese Technik nicht) und höheren Versionen.
Tipp 15: Zahlen formatieren
当你使用Watch或者Quick Watch窗口查看变量时,显示这些数值是用默认的预定义可视化格式。当变量是数字时,显示形式按照他们的类型(int、float、double)来的,并且使用十进制显示。然而,你可以设置调试器在显示数字的使用使用不同的类型,或者使用不同的进制。
改变变量显示类型可以在变量前添加以下前缀:
by —— unsigned char(unsigned byte)
wo —— unsigned short(unsigned word)
dw —— unsigned long(unsigned double word)
改变变量显示的进制可以在变量前添加以下前缀:
d或i —— 有符号十进制数
u —— 无符号十进制数
o —— 无符号八进制数
x —— 小写十六进制数
X —— 大写十六进制数
技巧16:格式化内存数据
除了数字,debugger还可以在Watch窗口中显示格式化的内存数据,最长为64字节。你可以在表达式(变量或者内存地址)后面添加下面的后缀来格式化数据:
mb或m —— 十六进制显示的16字节数据,后面跟着16个ASCII字符
mw —— 8字(WORD,通常1 WORD = 2 BYTE)数据
md —— 4个双字(DWORD,通常1 DWORD = 4 BYTE)数据
mq —— 2个四字(Quad WORD)数据
ma —— 64个ASCII字符
mu —— 2字节UNICODE字符
技巧17:在系统DLL调用处暂停
有时在DLL的某个函数被调用时暂停是很有用,特别是系统DLL(比如kernel32.dll、user32.dll)。实现这种暂停需要使用原生debugger提供的上下文运算符。你可以设定断点位置、变量名或者表达式:
{[函数],[源代码],[模块]}断点位置
{[函数],[源代码],[模块]}变量名
{[函数],[源代码],[模块]}表达式
大括号内可以是函数名、源代码及模块的任意组合,但是逗号不能省略。
举个例子如果我们需要在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的符号信息,可以获得全部调用堆栈信息,并且在调用堆栈窗口,使用上下文菜单(右键菜单),直接设置这种效果。你可以从预定义的符号路径或者微软的符号服务器(针对系统DLL)下载这些符号。在这些符号下载并导入到debugger中之后,调用堆栈更新如下:
这些符号也可以从Module窗口导入。
一旦载入之后,这些符号会保存在缓存中,并且可以在Tools>Options>Debugging>Symbols中配置。
技巧19:在MFC中报告内存泄露
如果你想在MFC应用程序中监测内存泄露,你可以使用宏DEBUG_NEW来重定义new运算符,这是new运算符的一个修改版本,可以记录其分配内存的文件名及行数。在Release版中构建的DEBUG_NEW会解析成原始的new运算符。
MFC向导产生的源代码中在#include后米娜包含如下预处理指令:
#ifdef _DEBUG #define new DEBUG_NEW #endif
上面代码就是如何重定义new运算符的方法。
很多STL头文件和这里定义的new运算符不兼容。如果你在重新定义运算符new之后包含了dab9f699790ab0922e596ecb9f6677d57d10b7d419803d4062679b45879052324309a73696dbaeac0ddd115cebb6f9b798c455a79ddfebb79781bff588e7b37e等头文件,会有如下错误(以7d10b7d419803d4062679b4587905232为例):
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> ]
解决方法是在包含这些STL文件之后再使用DEBUG_NEW重定义new运算符。
技巧20:调试ATL
当你开发ATL COM组件时你可以在debugger中查看你所开发的COM对象的QueryInterface、AddRef和Release的调用情况。默认情况下并不支持这些调用的产看,你需要在预处理定义或者预编译头文件中定义两个宏。这两个宏定义之后,关于这些函数的调用会显示在输出(Output)窗口中。
这两个宏是:
_ATL_DEBUG_QI,显示每个被查询接口的名字。必须在atlcom.h头文件被包含之前定义。
_ATL_DEBUG_INTERFACES,每当AddRef或Release被调用时显示当前接口的引用次数以及类名、接口名等信息。必须在atlbase.h包含之前定义。
以上就是本文的全部内容,希望大家结合之前分享过的文章进行学习,熟练掌握Visual Studio调试技巧。
更多分享Visual Studio原生开发的10个调试技巧(2)相关文章请关注PHP中文网!