社区应用 最新帖子 精华区 社区服务 会员列表 统计排行 银行

  • 31734阅读
  • 23回复

VC调试技巧大全

级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 20楼 发表于: 2013-09-06
10 More Visual Studio Debugging Tips for Native Development

I have recently run onto this article by Ivan Shcherbakov called 10+ powerful debugging tricks with Visual Studio. Though the article presents some rather basic tips of debugging with Visual Studio, there are others at least as helpful as those. Therefore I put together a list of ten more debugging tips for native development that work with at least Visual Studio 2008. (If you work with managed code, the debugger has even more features and there are several articles on CodeProject that present them.) Here is my list of additional tips:

For more debugging tips check the second article in the series, 10 Even More Visual Studio Debugging Tips for Native Development.

Tip 1: Break on Exception

It is possible to instruct the debugger to break when an exception occurs, before a handler is invoked. That allows you to debug your application immediately after the exception occurs. Navigating the Call Stack should allow you to figure the root cause of the exception.

Visual Studio allows you to specify what category or particular exception you want to break on. A dialog is available from Debug > Exceptions menu. You can specify native (or managed) exceptions and aside from the default exceptions known to the debugger, you can add your custom exceptions.

 

Here is an example with the debugger breaking when a std::exception is thrown.

 

Additional readings:

Tip 2: Pseudo-variables in Watch Windows

The Watch windows or the QuickWatch dialog support some special (debugger-recognized) variables called pseudovariables. The documented one include:

  • $tid – the thread ID of the current thread
  • $pid – the process ID
  • $cmdline – the command line string that launched the program
  • $user – information for the account running the program
  • $registername – displays the content of the register registername

However, one that is quite useful is a pseudo-variable for the last error:

  • $err – displays the numeric code of the last error
  • $err, hr – displays the message of the last error

 

Additional readings:

Tip 3: Watch Heap Objects After Symbol Goes out of Scope

Sometimes you'd like to watch the value of an object (on the heap) even after the symbol goes of scope. When that happens, the variable in the Watch window is disabled and cannot be inspected any more (nor updated) even if the object is still alive and well. It is possible to continue to watch it in full capability if you know the address of the object. You can then cast the address to a pointer of the object type and put that in the Watch window.

In the example bellow, _foo is no longer accessible in the Watch window after stepping out of do_foo(). However, taking its address and casting it to foo* we can still watch the object.

 

Tip 4: Watch a Range of Values Inside an Array

If you work with large arrays (let say at least some hundred elements, but maybe even less) expanding the array in the Watch window and looking for some particular range of elements is cumbersome, because you have to scroll a lot. And if the array is allocated on the heap you can't even expand its elements in the Watch window. There is a solution for that. You can use the syntax (array + <offset>), <count> to watch a particular range of <count> elements starting at the <offset> position (of course, array here is your actual object). If you want to watch the entire array, you can simply say array, <count>.

If your array is on the heap, then you can expand it in the Watch window, but to watch a particular range you'd have to use a slightly different the syntax: ((T*)array + <offset>), <count> (notice this syntax also works with arrays on the heap). In this case T is the type of the array's elements.

 

If you work with MFC and use the "array" containers from it, like CArray, CDWordArray, CStringArray, etc., you can of course apply the same filtering, except that you must watch the m_pData member of the array, which is the actual buffer holding the data.

 

Tip 5: Avoid Stepping into Unwanted Functions

Many times when you debug the code you probably step into functions you would like to step over, whether it's constructors, assignment operators or others. One of those that used to bother me the most was the CStringconstructor. Here is an example when stepping into take_a_string() function first steps into CString's constructor.

void take_a_string(CString const &text){}void test_string(){   take_a_string(_T("sample"));}

Luckily it is possible to tell the debugger to step over some methods, classes or entire namespaces. The way this was implemented has changed. Back in the days of VS 6 this used to be specified through the autoexp.dat file. Since Visual Studio 2002 this was changed to Registry settings. To enable stepping over functions you need to add some values in Registry (you can find all the details here):

  • The actual location depends on the version of Visual Studio you have and the platform of the OS (x86 or x64, because the Registry has to views for 64-bit Windows)
  • The value name is a number and represents the priority of the rule; the higher the number the more precedence the rules has over others.
  • The value data is a REG_SZ value representing a regular expression that specifies what to filter and what action to perform.

To skip stepping into any CString method I have added the following rule:

 

Having this enabled, even when you press to step into take_a_string() in the above example the debugger skips the CString's constructor.

Additional readings:

Tip 6: Launch the debugger from code

Seldom you might need to attach with the debugger to a program, but you cannot do it with the Attach window (maybe because the break would occur too fast to catch by attaching), nor you can start the program in debugger in the first place. You can cause a break of the program and give the debugger a chance to attach by calling the __debugbreak() intrinsic.

void break_for_debugging(){   __debugbreak();}

There are actually other ways to do this, such as triggering interruption 3, but this only works with x86 platforms (ASM is no longer supported for x64 in C++). There is also a DebugBreak() function, but this is not portable, so the intrinsic is the recommended method.

__asm int 3; 

When your program executes the intrinsic it stops, and you get a chance to attach a debugger to the process.

 

 

 

Additional readings: 

Tip 7: Print to Output Window

It is possible to show a particular text in the debugger's output window by calling DebugOutputString. If there is no debugger attached, the function does nothing.

Tip 8: Memory Leaks Isolation

Memory leaks are an important problem in native development and finding them could be a serious challenging especially in large projects. Visual Studio provides reports about detected memory leaks and there are other applications (free or commercial) to help you with that. In some situations though, it is possible to use the debugger to break when an allocation that eventually leaks is done. To do this however, you must find a reproducible allocation number (which might not be that easy though). If you are able to do that, then the debugger can break the moment that is performed.

Let's consider this code that allocates 8 bytes, but never releases the allocated memory. Visual Studio displays a report of the leaked objects, and running this several times I could see it's always the same allocation number (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.

The steps for breaking on a particular (reproducible) allocation are:

  • Make sure you have the adequate reporting mode for memory leaks (see Finding Memory Leaks Using the CRT Library).
  • Run the program several times until you find reproducible allocation numbers ({341} in my example above) in the memory leaks report at the end of running the program.
  • Put a breakpoint somewhere at the start of the program so you can break as early as possible.
  • Start the application with the debugger.
  • When the initial breakpoint is hit, in the watch window write in the Name column:{,,msvcr90d.dll}_crtBreakAlloc, and in Value column put the allocation number that you want to investigate.
  • Continue debugging (F5).
  • The execution stops at the specified allocation. You can use the Call Stack to navigate back to your code where the allocation was triggered.

Following these steps for my example with allocation number 341 I was able to identify the source of the leak:

 

Tip 9: Debug the Release Build

Debug and Release builds are meant for different purposes. While a Debug configuration is used for development, a Release configuration, as the name implies should be used for the final version of a program. Since it's supposed that the application meets the required quality to be published, such a configuration contains optimizations and settings that break the debugging experience of a Debug build. Still, sometimes you'd like to be able to debug the Release build the same way you debug the Debug build. To do that, you need to perform some changes in the configuration. However, in this case one could argue you no longer debug the Release build, but rather a mixture of the Debug and the Release builds.

 

There are several things you should do; the mandatory ones are:

  • C/C++ > General > Debug Information Format should be "Program Database (/Zi)"
  • C/C++ > Optimization > Optimization should be "Disabled (/Od)"
  • Linker > Debugging > Generate Debug Info should be "Yes (/DEBUG)"

 

Additional readings:

Tip 10: Remote Debugging

Another important debugging experience is remote debugging. This is a larger topic, covered many times, so I just want to summarize a bit.

  • You need Remote Debugging Monitor installed on the remote machine
  • The Remote Debugging Monitor must run "As Administrator" and the user must be a member of the Administrators group
  • When you run the monitor it starts a new server whose name you must use in the Visual Studio's Attach to Progress window in the Qualifier combo.

  • The firewalls on the remote and local machine must allow communication between Visual Studio and the Remote Debugging Monitor
  • To be able to debug, the PDB files are key; in order for the Visual Studio debugger to be able to load them automatically
    • the native PDBs must be available on the local machine (on the same path where the corresponding module is located on the remote machine),
    • the managed PDBs must be available on the remote machine.

Remote Debugging Monitor downloads:

Additional readings:

Conclusions

The debugging tips presented in this article and the original article that inspired this one should provide the necessary tips for most of the debugging experiences and problems. To get more information about these tips I suggest following the additional readings.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Marius Bancila
Software Developer (Senior) Visma Software 
Romania Romania
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 21楼 发表于: 2013-09-06
10 Even More Visual Studio Debugging Tips for Native Development

My previous article on debugging tips for Visual Studio was met with so much interest that determined me to share even more debugging techniques. Therefore you can find below a list of other helpful tips for debugging native applications (continuing the numbering from the previous article). These tips work with Visual Studio 2005 or newer (and at least some of them with older versions too). You can get additional information about each technique if you follow the recommended readings.

  1. Data breakpoints
  2. Renaming threads
  3. Breaking on particular threads
  4. (Roughly) Timing Execution
  5. Formatting numbers
  6. Formatting (memory) data
  7. Breaking on system DLLs
  8. Loading symbols
  9. Memory leak reports in MFC
  10. Debugging ATL

Tip 11: Data breakpoints

It is possible to instruct the debugger to break when data at a certain memory location changes. It is however only possible to create 4 such hardware data breakpoints at a time. Data breakpoints can be added only during debugging, either from the menu (Debug > New Breakpoint > New Data Breakpoint) or from the Breakpoints window.

You can use either a memory address or an expression that evaluates to an address. Even though you can watch both values on the stack and on the heap, I'd say this feature is mostly helpful to find when values on the heap are changed. This can be a great help in identifying memory corruption.

In the example below, the value of a pointer is changed, instead of the value of the object it points to. To figure out where that happens I set a breakpoint on the memory where the value of the pointer is stored, i.e. &ptr (notice this must happen after the pointer is initialized). When the data changes, which means someone alters the value of the pointer, the debugger breaks and I can figure which piece of code is responsible for that.

Additional readings:

Tip 12: Renaming threads

When you debug multi-threaded applications the Threads window shows you what threads are created and which one is currently running. The more threads you have the harder could be to figure out what thread exactly you are looking at (especially when the same thread procedure is run by several threads, and you don't know exactly which thread instance is currently executing).

The debugger allows you change the names of the threads. Use the context menu on a thread and rename it.

It is also possible to programmatically name a thread, though this is a little bit trickier and must be done after the thread has started, otherwise the debugger will re-initialize it with its default naming convention. Define and use the following function for renaming a thread.

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)	{	}}

Additional readings

Tip 13: Breaking on particular threads

Another helpful technique for multi-threading applications is to filter the breakpoints to certain threads, processes or even computers. This is possible by using the Filter command on a breakpoint.

The debugger allows you to specify any combination (AND, OR, NOT) of ThreadName, ThreadId, ProcessName, ProcessId and MachineName. Knowing how to set a thread name already can make this filtering even simpler.

Additional readings:

Tip 14: (Roughly) Timing Execution

In my previous article I wrote about pseudo-variables in the Watch window. One not mentioned then is @clk, which shows the value of a counter and can help getting a rough idea of how much time the code between two breakpoints took to execute. The value is expressed in microseconds. However, this is by no means a method to profile execution. You should use the Visual Studio profiler and/or performance timers for that.

It is possible to reset the clock by adding @clk=0 in the Watch window or executing that in the Immediate window. Therefore to figure how much time some piece of code took to execute do the following:

  • Set a breakpoint at the beginning of the block
  • Set a breakpoint at the end of the block
  • Add @clk in the Watch window
  • When the first breakpoint is hit type @clk=0 in the Intermediate window.
  • Run the program until the breakpoint at the end of the block is hit and check the value of @clk in the Watch window.

Notice there are tips on the web that instruct you to add two expressions in the Watch window: @clk followed by a @clk=0, which would allegedly reset the clock each time a breakpoint is hit. This used to worked in older versions of Visual Studio, but no longer works (for sure in VS2005 and newer).

Additional readings:

Tip 15: Formatting numbers

When you watch variables in the Watch or Quick Watch window, the values are displayed using the default pre-defined visualizers. When it comes to numbers, these are displayed according to their types (integer, float, double) and using the decimal base. But you can force the debugger to show the numbers as a different type, or with a different numeric base, or both.

To change the displayed type prefix the variable with:

  • by for unsigned char (aka unsigned byte)
  • wo for unsigned short (aka unsigned word)
  • dw for unsigned long (aka unsigned double word)

To change the displayed base suffix the variable name with:

  • , d or , i for signed decimal
  • , u for unsigned decimal
  • , o for unsigned octal
  • , x for lowercase hex or , X for uppercase hex
 

Additional readings:

Tip 16: Formatting (memory) data

Apart from numbers, the debugger can also show formatted memory values, up to 64 bytes, in the Watch window. You can use one of the following specifiers after an expression (variable or memory address) to format the data:

  • mb or m - 16 bytes in hex followed by 16 ASCII characters
  • mw - 8 words
  • md - 4 double words
  • mq - 2 quad-words
  • ma - 64 ASCII characters
  • mu - 2-byte UNICODE characters

Additional readings:

Tip 17: Breaking on system DLLs

Sometimes it's useful to break when some function in a DLL, like a system DLL (think kernel32.dll or user32.dll) is called. To do this one must use the context operator provided by the native debugger. You can qualify a breakpoint location, variable name or expression:

  • {[function],[source],[module] } location
  • {[function],[source],[module] } variable_name
  • {[function],[source],[module] } expression

The braces can contain any combination of function name, source and module, but the commas must not be omitted.

Let's say we want to break when CreateThread is called. This function is exported from kernel32.dll, and therefore the context operator should look like this: {,,kernel32.dll}CreateThread. However, this does not work as the operator requires the decorated name of CreateThread. One could use DBH.exe to figure out what the decorated name for a specific function is.

Here is how you can figure the decorated name for CreateThread:

C:\Program Files (x86)\Debugging Tools for Windows (x86)>dbh.exe -s:srv*C:\Symbols*http://msdl.microsoft.com/Download/Symbols -d C:\Windows\SysWOW64\kernel32.dll enum *CreateThread*Symbol Search Path: srv*C:\Symbols*http://msdl.microsoft.com/Download/Symbols indexaddress name 110b4f65 :   _BaseCreateThreadPoolThread@12 2102e6b7 :   _CreateThreadpoolWork@12 3103234c :   _CreateThreadpoolStub@4 41011ea8 :   _CreateThreadStub@24 51019d40 :   _NtWow64CsrBasepCreateThread@12 61019464 :   ??_C@_0BC@PKLIFPAJ@SHCreateThreadRef?$AA@ 7107309c :   ??_C@_0BD@CIEDBPNA@TF_CreateThreadMgr?$AA@ 8102ce87 :   _CreateThreadpoolCleanupGroupStub@0 91038fe3 :   _CreateThreadpoolIoStub@16 a102e6f0 :   _CreateThreadpoolTimer@12 b102e759 :   _CreateThreadpoolWaitStub@12 c102ce8e :   _CreateThreadpoolCleanupGroup@0 d102e6e3 :   _CreateThreadpoolTimerStub@12 e1038ff0 :   _CreateThreadpoolIo@16 f102e766 :   _CreateThreadpoolWait@1210102e6aa :   _CreateThreadpoolWorkStub@12111032359 :   _CreateThreadpool@4

Looks like the actual name is _CreateThreadStub@24. So we should create a breakpoint at{,,kernel32.dll}_CreateThreadStub@24.

Run the program and when it breaks, ignore the message that there is no source code associated with the breakpoint.

Use the Call Stack window to navigate to your code that made the call to the function.

Additional readings:

Tip 18: Loading symbols

When you debug your application the Call Stack window might not display a full call stack, but skip information about system DLLs (such as kernel32.dll and user32.dll).

It is possible to get the full stack by loading the symbols for these DLLs. This can be done directly from the Call Stack window, using the context menu. You can either download from the pre-specified symbol path, or from Microsoft's symbols server (if it's a system DLL). After the symbols are downloaded and loaded into the debugger, the Call Stack updates.

The symbols can also be loaded from the Modules window.

Once downloaded, the symbols are stored in a cache that can be configured from Tools > Options > Debugging > Symbols.

Tip 19: Memory leak reports in MFC

If you want to get memory leak reports in MFC application you can re-defined the new operator with a DEBUG_NEW macro, which is a modified version of the new operator that keeps track of the filename and line number for each object that it allocates. In a release build DEBUG_NEW resolves to operator new.

The wizard generated MFC source files contain the following pre-processor directives after the #includes:

#ifdef _DEBUG#define new DEBUG_NEW#endif

This is how you can re-define the new operator.

However, many STL headers are incompatible with this version of operator new. If you include <map>, <vector>, <list>, <string> and others after you re-define operator new you get errors like this (shown for <vector>):

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 types1>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 compiled1>with1>[1>_Ty=char1>]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>with1>[1>_Ty=char1>]1>c:\program files (x86)\microsoft visual studio 9.0\vc\include\xstring(2216) : see reference to class template instantiation 'std::allocator<_Ty>' being compiled1>with1>[1>_Ty=char1>]

The solution is to always include these STL headers before redefining new with DEBUG_NEW.

Additional readings: 

Tip 20: Debugging ATL

When you develop ATL COM components you can get some help from the debugger to watch calls for QueryInterface, AddRef and Release on your COM objects. This support is not enabled by default, but you can specify two macros to the preprocessor definitions or the pre-compiled header file. When those macros are defined information about these calls is displayed in the Output window.

These two macros are:

  • _ATL_DEBUG_QI displays the name of each interfaces that is queried for on your objects. It must be defined before atlcom.h is included.
  • _ATL_DEBUG_INTERFACES displays current reference count for the interface together with the class name and interface name, every time AddRef or Release is called. It must be defined before atlbase.h is included.

Additional readings:

Conclusions

The tips provided in this article and the previous one, although not everything that can be said about debugging, should prepare you for most debugging situations you could encounter with native applications.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Marius Bancila
Software Developer (Senior) Visma Software 
Romania Romania
Marius Bancila is a Microsoft MVP for VC++. He works as a software developer for Visma, a Norwegian-based company. He is mainly focused on building desktop applications with VC++ and VC#. He keeps a blog at http://www.mariusbancila.ro/blog, focused on Windows programming. He is the co-founder of codexpert.ro, a community for Romanian C++ programmers.
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 22楼 发表于: 2014-07-19
用Windbg和Visual Studio调试代码优劣比较
经常被问及这样的问题:  你为什么非要使用Windbg调试你的程序?  记得有一次面试的时候笔者也被问到这样的问题,我的回答是:不是我非要使用Windbg, 只是在不同的情况下使用不同的工具会更加方便而高效。     那么这两者到底有什么区别?在什么情况下用什么工具会更方便呢?     所谓尺有所短,寸有所长,那么笔者就总结一下在什么情况下用什么工具会更方便,可能不是100%的全面,但是应该可以说明一定的问题,希望给初学者以指引。     Case 1:  分析.dmp文件的时候,可以使用Windbg和Visual Studio,但是windbg更高效,.dmp文件是发生问题时的进程转储文件,windbg集成了一系列的基本命令和扩展命令,一句话,只要是.dmp文件里面有的,你都可以很快的得到。    


Case 2:
问题没有办法重现,就是说测试人员并不知道问题是怎么跑出来的,或者代码跑了几个月,跑出来一个问题,没有办法调试,只能抓一个.dmp文件,然后就回到了Case 1.    


Case 3:  内核调试,包括驱动,系统代码,他们本身是运行在内核级的,而我们的程序运行在用户态,如果希望调试这部分代码,Visual Studio似乎有些力不从心,这个时候就是windbg发挥的时候了。    


Case 4:  调试多线程死锁问题,用windbg很容易分析出来那个线程占用了那个锁。    


Case 5:  分布式,RPC调用,利用Visual Studio看那个组件调用哪个组件很不方便,而是用windbg却是一目了然。    


Case 6:  内存破坏问题,Visual Studio如果想查看内存破坏问题简直比登天还难,除非破坏完了马上crash,但是大部分的情况是内存破坏了,但是并不来表现出来,包括对破坏,栈破坏,windbg几个命令就可以轻松搞定。    


Case 7:  内存泄漏问题,结合umdh, leakdiag这两个工具,再加上windbg的内存分析,可以说没有查不出来的内存泄漏。     注意:Windbg调试.Net代码需要SOS扩展..    


总结   首先需要说明的是本文的目的并不是说Visual Studio和Windbg这两个工具哪个好哪个差,只是说在不同的场合不同的工具可能更加高效,请大家不要误解。另外Visual Studio绝对是Windows开发的利器, 在大多数Windows代码开发,调试方面具有不可替代的作用,如果说Visual Studio是利器,那么Windbg绝对是个神器,在一些极端的场合绝对可以独当一面。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 23楼 发表于: 2014-07-19
使用WinDbg调试程序
WinDbg是微软发布的一款相当优秀的源码级(source-level)调试工具,可以用于Kernel模式调试和用户模式调试,还可以调试Dump文件。
WinDbg是微软很重要的诊断调试工具: 可以查看源代码、设置断点、查看变量, 查看调用堆栈及内存情况。
 调试应用程序(用户模式 user mode)
 调试操作系统及驱劢程序(内核模式 kernel mode)
 调试非托管程序(native program)
 调试托管程序(managed program)
 实时调试 (JIT: Just in time)
 事后调试 (postmortem debugging)
使用WinDbg可以解决线上.NET应用程序的如下问题:
◆ 内存高
◆ CPU高
◆ 程序异常
◆ 程序Hang死

在生产环境下进行故障诊断时,为了不终止正在运行的服务或应用程序,有两种方式可以对正在运行的服务或应用程序的进程进行分析和调试。
一、用WinDbg等调试器直接attach到需要调试的进程,调试完毕之后再detach即可。但是这种方式有个缺点就是执行debugger命令时必须先break这个进程,执行完debug命令之后又得赶紧F5让他继续运 行,因为被你break住的时候意味着整个进程也已经被你挂起。另外也经常会由于First Chance Excetpion而自动break,你得时刻留意避免长时间break整个进程。所以这样的调试方式对时间是个很大的考验,往往没有充裕的时间来做仔细分析。
二、在出现问题的时候,比如CPU持续长时间100%,内存突然暴涨等非正常情况下,通过对服务进程snapshot抓取一个dump文件,完成dump之后先deatch,让进程继续运行。然后用windbg等工具来分析这个抓取到的dump 文件。所以我们一般采用这种方式来进行调试排错。

设置符号文件目录
符号文件包含了相关二进制文件的调试信息以.pdb戒.dbg为扩展名。WinDbg使用符号文件来确定调用栈,堆及其他重要信息。
配置WinDbg的符号文件路径
WinDbg符号文件路径搜索的两个位置:环境变量中的_NT_SYMBOL_PATH设置及WinDbg中的"symblos file path";
设置srv*x:/symbols_folder*http://msdl.microsoft.com/download/symbols 路径是保证我们能快速正确使用windbg的法。
1、运行WinDbg->File->Symbol File Path->按照下面的方法设置_NT_SYMBOL_PATH变量:
在弹出的框中输入"C:\ Symbols; SRV*C:\MyLocalSymbols*http://msdl.microsoft.com/download/symbols"(按照这样设置,WinDbg将先从本地文件夹C:\ Symbols中查找Symbol,如果找不到,则自动从MS的Symbol Server上下载Symbols)。另一种做法是从这个Symbol下载地址中http://www.microsoft.com/whdc/devtools/debugging/symbolpkg.mspx,下载相应操作系统所需要的完整的Symbol安装包,并进行安装,例如我将其安装在D:\WINDOWS\Symbols,在该框中输入"D:\WINDOWS\Symbols"。(这里要注意下载的Symbols的版本一定要正确
2、在控制板的系统中设置一个系统变量_NT_SYMBOL_PATH 为
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols

dump文件获取
dump文件是进程的内存镜像。可以把程序的执行状态,即当时程序内存空间数据通过调试器保存到dump文件中。
1、利用WinDbg里的adplus来获取dump文件
Adplus.vbs 是一个Visual Basic Script 文件,Adplus 主要用来生成内存转储文件 (dump file),内存转储文件适用于不能实时调试的情况下。在WinDbg安装目录里可以找到adplus.vbs,使用adplus.vbs生成dump文件,
adplus -hang -o d:\dump -p 1234
其中hang表示附加到进程,如果是crash,则为目标进程崩溃的时候抓取,-o后面的参数表示dump文件存到位置,-p后面的数字为进程的PID,也可以是-pn后面跟进程名称,如:adplus.vbs -hang -pn ConsoleWindbg.exe -o D:\dump
2、使用Debug Diagnostic Tool(DebugDiag)工具获取dump文件
下载Debug Diagnostic Tool然后进行安装,打开该工具,Debug Diagnostic Tool可以选择不同的规则来进行dump文件。可以根据程序崩溃时捕获dump文件,也可以根据性能指标来进行捕获,如CPU过高,死锁,HTTP响应时间过程等参数。如下图:

也可以找到对应的进程,通过如下方法进行捕获。此种方式获取的dump文件放到C:\Program Files\DebugDiag\Logs\Misc下。

3、使用.dump命令
1) 打开WinDBG—>File—>Attach to a Process,然后选择将之要进行捕获的进程。如我们这里要对ConsoleWindbg.exe进程产生dump文件。选择后如图:



2)在上图红色区域的输入框内输入产生dump 文件的命令 .dump 可以选择不同的参数来生成不同类型的dump文件。
选项(1): /m
命令行示例.dump /m D:/dump/myapp.dmp
注解: 缺省选项,生成标准的minidump, 转储文件通常较小,便于在网络上通过邮件或其他方式传输。 这种文件的信息量较少,只包含系统信息、加载的模块(DLL)信息、 进程信息和线程信息。
选项(2): /ma
命令行示例.dump /ma D:/dump/myapp.dmp
注解: 带有尽量多选项的minidump(包括完整的内存内容、句柄、未加载的模块,等等),文件很大,但如果条件允许(本机调试,局域网环境), 推荐使用这中dump。
选项(3):/mFhutwd
命令行示例.dump /mFhutwd D:/dump/myapp.dmp
注解:带有数据段、非共享的读/写内存页和其他有用的信息的minidump。包含了通过minidump能够得到的最多的信息。是一种折中方案。
4、使用ProcDump工具
Procdump是一个轻量级的命令行工具, 它的主要目的是监控应用程序的CPU异常动向, 并在此异常时生成crash dump文件, 供研发人员和管理员确定问题发生的原因。你还可以把它作为生成dump的工具使用在其他的脚本中。有了它, 就完全不需要在同一台服务器上使用诸如32位系统上的Debug Diag 1.1或是64位系统上的ADPlus了。
Procdump下载:http://technet.microsoft.com/en-us/sysinternals/dd996900
procdump -ma -c 50% -s 3 -n 2 5844 (Process Name or PID) -o c:\dumpfile
-ma 生成full dump, 即包括进程的所有内存. 默认的dump格式包括线程和句柄信息。
-c 在CPU使用率到达这个阀值的时候, 生成dump文件。
-s CPU阀值必须持续多少秒才抓取dump文件。
-n 在该工具退出之前要抓取多少个dump文件。
-o dump文件保存目录。

技术术语
GC Heap:用于存储对象实例,受 GC 管理
Loader Heap:分为 High-Frequency Heap 、 Low-Frequency Heap 和 Stub Heap ,不同的 heap 又存储不同的信息。 Loader Heap 中最重要的信息是元数据 (MetaData) 相关的信息,也就是 Type 对象,每个 Type 对象在 Loader Heap 上体现为一个 Method Table , Method Table 中记录了存储的元数据信息,如基类型、静态字段、实现的接口、所有的方法等。 Loader Heap 的生命周期为从 AppDomain 创建到卸载。
MethodTable: 我们知道每种type可以有多个instance,每个instance,其每个field可以享有独立的space,而对于type的method提供一个公共的method入口地址。也就是说不管多少个相同类型的instance,其都指向了同一个同一的函数入口地址。在这个函数入口地址描述表中记录了各个函数的入口地址。而MethodTable就有点类似的作用。不过所有Assembly都是自描述的,因此我们可以从MethodTable中,可以知道相应的instance。因此通过相应的debug命令!dumpheap -mt MTAddress可以知道在MethodTable中相关联的所有instance了。
Finalization 原理

通过WinDbg分析dump文件
通过上面步骤,我们生成了dump文件,接下来我们就可以使用WinDbg工具对生成的dump文件进行分析。
案例:
建立控制台应用程序,代码如下:
namespace ConsoleWindbg
{
class Program
{
private static List<User> list =new List<User>();
static void Main(string[] args)
{
MemeryLeakProc();
Console.ReadLine();
}

private static void MemeryLeakProc()
{
string str = "aaa";
while (true)
{
for (int i = 0; i < 100 * 1024; i++)
{
str += "bbb" + i;
User u = new User();
u.Age = i;
u.Name = "UserName" + i;
list.Add(u);
}
Thread.Sleep(1000);
}
}

}

public class User
{
public int Age { set; get; }
public string Name { set; get; }
}
}
编译,运行,按照上面的步骤产生dump文件。然后使用WinDbg打开dump文件。

红色标注区域显示了dump文件获取的一些环境信息,如:当前系统信息,程序运行时间,符号文件的路径等。
WinDbg调试托管程序时需用SOS扩展(SOS.dll), SOS 调试扩展(SOS.dll) 通过提供有关内部公共语言运行时(CLR) 环境的信息,帮助您在WinDbg.exe 调试器和Visual Studio 中调试托管程序。SOS.dll安装在.Net Framewok 目录底下C:\Windows\Microsoft.NET\Framework\vx.x.xxxxx。WinDbg调用SOS.dll的语法:
SOS.dll 在.Net Framewok 目录底下,在WinDbg的命令行输入:
.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll
拷贝SOS.dll 到windbg目录底下,注意拷贝的Framework版本必须和你要调试的目标程序所使用的版本一致,否则调试信息就不能正确显示出来。如果你同时工作在两个版本的Framework的话,可以SOS文件重命名为SOS<version>.dll或者直接将它们放入不同的文件夹下面。可以使用如下命令:
.load sos.dll
.loadby sos mscorwks  [.Net 3.5版本及以下]
     检查SOS.dll是否已经装载
    .chain
内存过高问题
内存过高问题初判定,内存泄漏可以通过以下两种方式通过性能监视器来基本判断属于那种类型的内存泄露。
A、非托管程序的症状 (Perfmon工具)
Process\Private Bytes 增加
.NET CLR Memory\# Bytes in all heaps不增加
B、托管程序的症状 (Perfmon工具)
Process\Private Bytes增加
.NET CLR Memory\# Bytes in all heaps也增加
本例属于托管程序症状,通过Perfmon工具已经得到判断,此步骤略。
    测试程序
    BuggyBits(http://blogs.msdn.com/cfs-filesystemfile.ashx/__key/communityserver-components-postattachments/00-07-43-14-54/BuggyBits.zip
    下载后部署到本机,产生压力:
    tinyget -srv:dbg.buggybit.com -uri:/Links.aspx -loop:4000
    观察进程中w3wp进程,发现内存增长很快,大约该进程内存增长到700M时,抓取一个hang dump。
    与内存操作相关命令
    !eeheap –gc:正是查看GC堆得命令,查看GC堆上的内存占用是多大
    !eeheap –loader:正是查看Loader堆得命令
    !dumpheap –stat:就是GC堆上的统计,就看看GC堆上存活的对象是那些
    !dumpheap -mt <<MethodTable address>>:查看该地址上的对象
    !dumpheap –type:通过 type 参数查看内存中指定类型的对象
    !gcroot +对象地址:这个命令就可以得到这个对象的"根"
    !objsize +对象地址:查看对象占用多大的内存
    !name2ee TestClass.exe TestClass.Program.test ://显示test方法相关的地址
    !dumpmt -md 00976d48 ://得到类的成员函数详细信息
    !dumpmt:找到相关MethodTable处的相关信息。
    !dumpmd:根据MethodDesc找到相关模块信息,比如MethodTable.
    !dumpdomain:显示所有域里的程序集,或者根据参数获取指定域。
    !dumpil 00973028:// 显示这个方法被编译器编译之后的IL代码
    !dumpobj(do) 012a3904: //显示一个对象的具体内容,看对象里面有什么,值是什么
    !dumpmodule 1ee30010:查看某个模块的详细信息
    !DumpArray: //查看数组信息
    分析过程
3.1、运行命令!eeheap –gc查看GC堆的情况,发现GC Heap大小为720多兆,所以我们重点分析托管堆的情况。从运行结果可以看到GC Heap中g0,g1,g2和LOH的堆情况,以及该GC Heap中所分配的段情况。

可以运行!dumpheap -mt 0c3b0038 0c3b0048命令查看LOH堆中大对象的情况。从统计结果看,LOH堆中没有大的对象存在。同理我们也可以统计各个段上对象的情况。

3.2、接下来看下heap中对象的一些情况,运行命令!dumpheap –stat。统计堆上所有对象的情况。统计项包括MT(Method Table),Count对象个数,TotalSize对象所占用的大小。Count与TotalSize按照升序统计。

最终我们发现,System.Char[]占用内存最多,大概720M,同时有36088个System.Char[]对象。

这里我们做个推理,通过!dumpheap –stat统计到的System.Char[]的个数,应该与在3.1中显示的各个段中System.Char[]个数之和相等。即如果对3.1中统计到的各个段进行!dumpheap –stat <startAddress> <endAddress>统计,各个段中统计到的System.Char[]个数之和应该与3.2中统计到的结果相同,通过验证发现,结论正确。
3.3、过滤一下,看看10K以上大小的字符串,运行命令:!dumpheap -mt 6f021ee4 -min 10000。10K以上的有35996个。

3.4、随便找个对象看下引用关系,运行!gcroot 36278028,结果如下:

通过结果发现,Link引用了这个字符串。而且我们看到,link是在Finalizer Queue中的。有关Finalizer Queue可以参考.net Finalization原理。
3.5、通过运行命令! Finalizequeue 查看Finalizer Queue队列的情况。
00b740fc 35987 575792 Link
一共有35987个Link对象存在于Finalizer Queue中,因此可以判定,Link类一定是显示的实现了Finalize方法。
3.6、查看该方法,代码如下:
  ~Link()
  {
  //some long running operation when cleaning up the data
  Thread.Sleep(5000);
}
3.7、接下来我们看下Link对象的结构,可以通过3.4步骤中运行出来的结果找到对应那个Link对象的地址,通过运行命令!do 36277ffc 来查看,当然也可以通过找到Link对象的MT,通过查看!dumpheap –mt <MTAddress>上的所有Link对象,找到其中一个地址,在通过!do <address>来查看。

据此发现,Link应该有url和name两个属性。通过!objsize 362a66ec查看url对象的大小为20k,且是StringBuilder类型的。
sizeof(362a66ec) = 20040 ( 0x4e48) bytes (System.Text.StringBuilder)
3.8、查看代码
public Link(string name, string url)
  {
  this.name = name;
  this.url.Append(url);
  }
会引起垃圾回收器托管堆速度的几个问题
 1、分配太频繁
 2、预先分配空间
 3、太多的引用(pointers)和根(roots)
 4、太多的对象实例有很长的生命期
 5、太多的定位对象实例(pinned)
6、有终结函数的对象实例
         占用更多资源
         更长的生命期
         两次才能回收
         垃圾回收器(GC)只有一个线程来运行终结函数
         有时这个线程会很慢戒堵塞(blocked)
CPU/异常操作相关命令
查看引起CPU过高命令比如:
!threadpool:查看线程池CPU使用量,我认为WEB的比如iis应用程序池进程w3wp如果CPU使用过高,那查看线程池命令肯定看的出来过高,这个是我自己的理解,c/s的就不一定了。
!threads:查看所有托管线程情况
!clrstack:到具体某个线程后,本线程托管代码的调用栈情况
~* e !clrstack:所有线程托管代码的调用栈情况
!runaway:查看线程占用CPU时间,可以从中找到哪个线程占用时间更高。
~number s:number为具体哪个线程的ID。
!dumpstackobjects(!dso):本线程调用栈所有对象实例
!dumpdomain:显示所有域里的程序集,或者根据参数获取指定域。
!savemodule:根据具体程序集地址,把当前程序集的代码生成到指定文件
!PrintException:显示在当前线程上引发的最后一个异常错误信息
!StopOnException:在指定异常错误信息停止运行
!VerifyHeap:检查垃圾回收器堆中是否有损坏迹象,并显示找到任何错误
!SyncBlk –all:显示所有SyncBlock 结构情况

4.1、产生压力
TinyGet.exe -srv:dbg.buggybit.com -uri:/AllProducts.aspx -threads:5 -loop:1
4.2、通过Procdump抓取三个dump文件
procdump -ma -c 50% -s 2 -n 3 w3wp.exe -o d:\dump
4.3、打开这三个dump,加载sos之后,分别查看!runaway的输出。
第一个dump输出:
0:023> !runaway
User Mode Time
Thread Time
23:14c8 0 days 0:00:06.817
22:1b74 0 days 0:00:06.084
31:ba8 0 days 0:00:02.823
30:680 0 days 0:00:02.823
33:25c 0 days 0:00:00.280
35:13c8 0 days 0:00:00.218
第二个dump输出:
0:023> !runaway
User Mode Time
Thread Time
23:14c8 0 days 0:00:12.792
22:1b74 0 days 0:00:11.918
31:ba8 0 days 0:00:04.009
30:680 0 days 0:00:03.712
35:13c8 0 days 0:00:01.965
33:25c 0 days 0:00:01.887
34:14e4 0 days 0:00:00.514    
第三个dump输出:
0:023> !runaway
User Mode Time
Thread Time
23:14c8 0 days 0:00:18.969
22:1b74 0 days 0:00:17.160
31:ba8 0 days 0:00:05.382
30:680 0 days 0:00:04.804
35:13c8 0 days 0:00:03.151
33:25c 0 days 0:00:02.792
34:14e4 0 days 0:00:01.185
4.4、从上面三个输出结果发现只有22,23,30,31号线程一值在增长。且22,23线程增长的速度较快。查看三个dump的!threadpool基本都在90%以上。
4.5、运行!threads查看当前都有哪些线程。

不知道为什么并没有找到22,23线程,有30,31线程号。
4.6、切换到30线程中,运行命令~30s
4.7、查看当前线程的调用栈情况,运行!clrstack

发现System.String.Concat方法,这是典型的字符串拼接的函数,通过调用关系发现应该是在AllProducts.Page_Load(System.Object, System.EventArgs)方法中。
4.8、查看代码
protected void Page_Load(object sender, EventArgs e)
{
DataTable dt = ((DataLayer)Application["DataLayer"]).GetAllProducts();
string ProductsTable = "<table><tr><td><B>Product ID</B></td><td><B>Product Name</B></td><td><B>Description</B></td></tr>";

foreach (DataRow dr in dt.Rows)
{
ProductsTable += "<tr><td>" + dr[0] + "</td><td>" + dr[1] + "</td><td>" + dr[2] + "</td></tr>" ;
}
ProductsTable += "</table>";
tblProducts.Text = ProductsTable;
}
这里面有一个循环的方法,然后针对输出的DataTable,进行了大量的String.Concat操作。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
描述
快速回复

您目前还是游客,请 登录注册
如果您提交过一次失败了,可以用”恢复数据”来恢复帖子内容