1.msdn 在debug模式下的內存結構
(曾今在gaia引擎裡看過類似的自己模仿實現的內存管理結構)
typedef struct _CrtMemBlockHeader
{
// Pointer to the block allocated just before this one:
struct _CrtMemBlockHeader *pBlockHeaderNext;
// Pointer to the block allocated just after this one:
struct _CrtMemBlockHeader *pBlockHeaderPrev;
char *szFileName; // File name
int nLine; // Line number
size_t nDataSize; // Size of user block
int nBlockUse; // Type of block
long lRequest; // Allocation number
// Buffer just before (lower than) the user's memory:
unsigned char gap[nNoMansLandSize];
} _CrtMemBlockHeader;
/* In an actual memory block in the debug heap,
* this structure is followed by:
* unsigned char data[nDataSize];
* unsigned char anotherGap[nNoMansLandSize];//防止內存寫越界
*/
The “NoMansLand” buffers on either side of the user data area of the block are currently 4 bytes in size, and are filled with a known byte value used by the debug heap routines to verify that the limits of the user’s memory block have not been overwritten. The debug heap also fills new memory blocks with a known value. If you elect to keep freed blocks in the heap’s linked list as explained below, these freed blocks are also filled with a known value. Currently, the actual byte values used are as follows:
NoMansLand (0xFD)
The “NoMansLand” buffers on either side of the memory used by an application are currently filled with 0xFD.
Freed blocks (0xDD)
The freed blocks kept unused in the debug heap’s linked list when the _CRTDBG_DELAY_FREE_MEM_DF flag is set are currently filled with 0xDD.
New objects (0xCD)
New objects are filled with 0xCD when they are allocated.
2._CrtDumpMemLeaks()
msdn說明
The _CrtDumpMemoryLeaks function determines whether a memory leak has occurred since the start of program execution. When a leak is found, the debug header information for all of the objects in the heap is dumped in a user-readable form. When _DEBUG is not defined, calls to _CrtDumpMemoryLeaks are removed during preprocessing.
_CrtDumpMemoryLeaks is frequently called at the end of program execution to verify that all memory allocated by the application has been freed. The function can be called automatically at program termination by turning on the _CRTDBG_LEAK_CHECK_DF bit field of the _crtDbgFlag flag using the _CrtSetDbgFlag function.
_CrtDumpMemoryLeaks calls _CrtMemCheckpoint to obtain the current state of the heap and then scans the state for blocks that have not been freed. When an unfreed block is encountered, _CrtDumpMemoryLeaks calls _CrtMemDumpAllObjectsSince to dump information for all of the objects allocated in the heap from the start of program execution.
By default, internal C run-time blocks (_CRT_BLOCK ) are not included in memory dump operations. The _CrtSetDbgFlag function can be used to turn on the _CRTDBG_CHECK_CRT_DF bit of _crtDbgFlag to include these blocks in the leak detection process.
For more information about heap state functions and the _CrtMemState structure, see the Heap State Reporting Functions . For information about how memory blocks are allocated, initialized, and managed in the debug version of the base heap, see Memory Management and the Debug Heap .
應用:
#include "stdafx.h"
#include <assert.h>
#ifdef _DEBUG
#define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, __LINE__)
#else
#define DEBUG_CLIENTBLOCK
#endif
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG
#define new DEBUG_CLIENTBLOCK
#endif //此部分用於使_CrtDumpMemoryLeaks輸出內存洩漏文件名和行號信息默認不會輸出相關信息
void Exit
{
int i = _CrtDumpMemoryLeaks;
assert( i == 0);
}
int _tmain(int argc, _TCHAR* argv[])
{
atexit(Exit);
int* p = new int;
return 0;
}
不含紅色部分輸出:
Detected memory leaks!
Dumping objects ->
{112} normal block at 0x003AA770, 4 bytes long.
Data: < > 00 00 00 00
Object dump complete.
含紅色部分輸出:
Detected memory leaks!
Dumping objects ->
d:\code\consoletest\consoletest.cpp(21) : {112} client block at 0x003A38B0, subtype 0, 4 bytes long.
Data: < > 00 00 00 00
Object dump complete.
-------------------------------------------------------------------------------------------
1._CrtDumpMemoryLeaks
確定自程序開始執行以來是否發生過內存洩漏,如果發生過,則轉儲所有已分配對象。如果已使用 _CrtSetDumpClient 安裝了掛鉤函數,那麼,_CrtDumpMemoryLeaks每次轉儲 _CLIENT_BLOCK 塊時,都會調用應用程序所提供的掛鉤函數。
CrtDumpMemoryLeaks()就是顯示當前的內存洩漏。 注意是“當前”,也就是說當它執行時,所有未銷毀的對象均會報內存洩漏。因此盡量讓這條語句在程序的最後執行。它所反映的是檢測到洩漏的地方。
一般用在MFC中比較准確,在InitInstance裡面調用_CrtDumpMemoryLeaks
2.信息輸出
Detected memory leaks!
Dumping objects ->
{52} normal block at 0x006D2498, 512 bytes long.
?Data: <??????????????? > 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
{51} normal block at 0x006D2440, 24 bytes long.
?Data: < 4????????????? > 10 34 14 00 FF FF FF FF 00 00 00 00 00 00 00 00
Object dump complete.
3._CrtSetBreakAlloc
知道某個錯誤分配塊的分配請求編號後,可以將該編號傳遞給 _CrtSetBreakAlloc 以創建一個斷點
_CrtSetBreakAlloc(51);這樣可以快速在{51}次內存洩漏處設上斷點。
/*****************************************************************************************************/
最快速度找到內存洩漏
許式偉
2006年11月某日
內存管理是C++程序員的痛。我的《內存管理變革 》系列就是試圖討論更為有效的內存管理方式,以杜絕(或減少)內存洩漏,減輕C++程序員的負擔。由於工作忙的緣故,這個系列目前未完,暫停。
這篇短文我想換個方式,討論一下如何以最快的速度找到內存洩漏。
確認是否存在內存洩漏
我們知道,MFC程序如果檢測到存在內存洩漏,退出程序的時候會在調試窗口提醒內存洩漏。例如:
class CMyApp : public CWinApp
{
public :
BOOL InitApplication()
{
int * leak = new int [ 10 ];
return TRUE;
}
};
產生的內存洩漏報告大體如下:
Detected memory leaks !
Dumping objects ->
c:\work\test.cpp( 186 ) : { 52 } normal block at 0x003C4410 , 40 bytes long .
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
這挺好。問題是,如果我們不喜歡MFC,那麼難道就沒有辦法?或者自己做?
呵呵,這不需要。其實,MFC也沒有自己做。內存洩漏檢測的工作是VC++的C運行庫做的。也就是說,只要你是VC++程序員,都可以很方便地檢測內存洩漏。我們還是給個樣例:
#include < crtdbg.h >
inline void EnableMemLeakCheck()
{
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
void main()
{
EnableMemLeakCheck();
int * leak = new int [ 10 ];
}
運行(提醒:不要按Ctrl+F5,按F5),你將發現,產生的內存洩漏報告與MFC類似,但有細節不同,如下:
Detected memory leaks !
Dumping objects ->
{ 52 } normal block at 0x003C4410 , 40 bytes long .
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
為什麼呢?看下面。
定位內存洩漏由於哪一句話引起的
你已經發現程序存在內存洩漏。現在的問題是,我們要找洩漏的根源。
一般我們首先確定內存洩漏是由於哪一句引起。在MFC中,這一點很容易。你雙擊內存洩漏報告的文字,或者在Debug窗口中按F4,IDE就幫你定位到申請該內存塊的地方。對於上例,也就是這一句:
int* leak = new int[10];
這多多少少對你分析內存洩漏有點幫助。特別地,如果這個new僅對應一條delete(或者你把delete漏寫),這將很快可以確認問題的症結。
我們前面已經看到,不使用MFC的時候,生成的內存洩漏報告與MFC不同,而且你立刻發現按F4不靈。那麼難道MFC做了什麼手腳?
其實不是,我們來模擬下MFC做的事情。看下例:
inline void EnableMemLeakCheck()
{
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
void main()
{
EnableMemLeakCheck();
int * leak = new int [ 10 ];
}
再運行這個樣例,你驚喜地發現,現在內存洩漏報告和MFC沒有任何分別了。
快速找到內存洩漏
單確定了內存洩漏發生在哪一行,有時候並不足夠。特別是同一個new對應有多處釋放的情形。在實際的工程中,以下兩種情況很典型:
創建對象的地方是一個類工廠(ClassFactory)模式。很多甚至全部類實例由同一個new創建。對於此,定位到了new出對象的所在行基本沒有多大幫助。
COM對象。我們知道COM對象采用Reference Count維護生命周期。也就是說,對象new的地方只有一個,但是Release的地方很多,你要一個個排除。
那麼,有什麼好辦法,可以迅速定位內存洩漏?
答:有。
在內存洩漏情況復雜的時候,你可以用以下方法定位內存洩漏。這是我個人認為通用的內存洩漏追蹤方法中最有效的手段。
我們再回頭看看crtdbg生成的內存洩漏報告:
Detected memory leaks !
Dumping objects ->
c:\work\test.cpp( 186 ) : { 52 } normal block at 0x003C4410 , 40 bytes long .
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
除了產生該內存洩漏的內存分配語句所在的文件名、行號為,我們注意到有一個比較陌生的信息:{52}。這個整數值代表了什麼意思呢?
其實,它代表了第幾次內存分配操作。象這個例子,{52}代表了第52次內存分配操作發生了洩漏。你可能要說,我只new過一次,怎麼會是第52次?這很容易理解,其他的內存申請操作在C的初始化過程調用的呗。:)
有沒有可能,我們讓程序運行到第52次內存分配操作的時候,自動停下來,進入調試狀態?所幸,crtdbg確實提供了這樣的函數:即 long _CrtSetBreakAlloc (long nAllocID)。我們加上它:
inline void EnableMemLeakCheck()
{
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
void main()
{
EnableMemLeakCheck();
_CrtSetBreakAlloc ( 52 );
int * leak = new int [ 10 ];
}
你發現,程序運行到 int * leak = new int [ 10 ]; 一句時,自動停下來進入調試狀態。細細體會一下,你可以發現,這種方式你獲得的信息遠比在程序退出時獲得文件名及行號有價值得多。因為報告洩漏文件名及行號,你獲得的只是靜態的信息,然而_CrtSetBreakAlloc 則是把整個現場恢復,你可以通過對函數調用棧分析(我發現很多人不習慣看函數調用棧,如果你屬於這種情況,我強烈推薦你去補上這一課,因為它太重要了)以及其他在線調試技巧,來分析產生內存洩漏的原因。通常情況下,這種分析方法可以在5分鐘內找到肇事者。
當然,_CrtSetBreakAlloc 要求你的程序執行過程是可還原的(多次執行過程的內存分配順序不會發生變化)。這個假設在多數情況下成立。不過,在多線程的情況下,這一點有時難以保證。
附加說明:
對“內存管理”相關的技術感興趣?這裡可以看到我的所有關於內存管理的文章 。
/****************************************************************************************************************/
VC使用CRT調試功能來檢測內存洩漏
作者:JerryZ
C/C++ 編程語言的最強大功能之一便是其動態分配和釋放內存,但是中國有句古話:“最大的長處也可能成為最大的弱點”,那麼 C/C++ 應用程序正好印證了這句話。在 C/C++ 應用程序開發過程中,動態分配的內存處理不當是最常見的問題。其中,最難捉摸也最難檢測的錯誤之一就是內存洩漏,即未能正確釋放以前分配的內存的錯誤。偶爾發生的少量內存洩漏可能不會引起我們的注意,但洩漏大量內存的程序或洩漏日益增多的程序可能會表現出各種各樣的征兆:從性能不良(並且逐漸降低)到內存完全耗盡。更糟的是,洩漏的程序可能會用掉太多內存,導致另外一個程序垮掉,而使用戶無從查找問題的真正根源。此外,即使無害的內存洩漏也可能殃及池魚。
幸運的是,Visual Studio 調試器和 C 運行時 (CRT) 庫為我們提供了檢測和識別內存洩漏的有效方法。下面請和我一起分享收獲——如何使用 CRT 調試功能來檢測內存洩漏?
一、如何啟用內存洩漏檢測機制
VC++ IDE 的默認狀態是沒有啟用內存洩漏檢測機制的,也就是說即使某段代碼有內存洩漏,調試會話的 Output 窗口的 Debug 頁不會輸出有關內存洩漏信息。你必須設定兩個最基本的機關來啟用內存洩漏檢測機制。
一是使用調試堆函數:
#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h>
注意:#include 語句的順序。如果更改此順序,所使用的函數可能無法正確工作。
通過包含 crtdbg.h 頭文件,可以將 malloc 和 free 函數映射到其“調試”版本 _malloc_dbg 和 _free_dbg,這些函數會跟蹤內存分配和釋放。此映射只在調試(Debug)版本(也就是要定義 _DEBUG)中有效。發行版本(Release)使用普通的 malloc 和 free 函數。#define 語句將 CRT 堆函數的基礎版本映射到對應的“調試”版本。該語句不是必須的,但如果沒有該語句,那麼有關內存洩漏的信息會不全。
二是在需要檢測內存洩漏的地方添加下面這條語句來輸出內存洩漏信息:
_CrtDumpMemoryLeaks();
當在調試器下運行程序時,_CrtDumpMemoryLeaks 將在 Output 窗口的 Debug 頁中顯示內存洩漏信息。比如: Detected memory leaks!
Dumping objects ->
C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {44} normal
block at 0x00441BD0, 33 bytes long.
Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD
c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {43} normal
block at 0x00441C20, 40 bytes long.
Data: < C > 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00
Object dump complete.
如果不使用 #define _CRTDBG_MAP_ALLOC 語句,內存洩漏的輸出是這樣的:
Detected memory leaks!
Dumping objects ->
{45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
{44} normal block at 0x00441BD0, 33 bytes long.
Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD
{43} normal block at 0x00441C20, 40 bytes long.
Data: < C > C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00
Object dump complete.
根據這段輸出信息,你無法知道在哪個源程序文件裡發生了內存洩漏。下面我們來研究一下輸出信息的格式。第一行和第二行沒有什麼可說的,從第三行開始:
xx}:花括弧內的數字是內存分配序號,本文例子中是 {45},{44},{43};
block:內存塊的類型,常用的有三種:normal(普通)、client(客戶端)或 CRT(運行時);本文例子中是:normal block;
用十六進制格式表示的內存位置,如:at 0x00441BA0 等;
以字節為單位表示的內存塊的大小,如:32 bytes long;
前 16 字節的內容(也是用十六進制格式表示),如:Data: 41 42 等;
仔細觀察不難發現,如果定義了 _CRTDBG_MAP_ALLOC ,那麼在內存分配序號前面還會顯示在其中分配洩漏內存的文件名,以及文件名後括號中的數字表示發生洩漏的代碼行號,比如:
C:\Temp\memleak\memleak.cpp(15)
雙擊 Output 窗口中此文件名所在的輸出行,便可跳到源程序文件分配該內存的代碼行(也可以選中該行,然後按 F4,效果一樣) ,這樣一來我們就很容易定位內存洩漏是在哪裡發生的了,因此,_CRTDBG_MAP_ALLOC 的作用顯而易見。
使用 _CrtSetDbgFlag
如果程序只有一個出口,那麼調用 _CrtDumpMemoryLeaks 的位置是很容易選擇的。但是,如果程序可能會在多個地方退出該怎麼辦呢?在每一個可能的出口處調用 _CrtDumpMemoryLeaks 肯定是不可取的,那麼這時可以在程序開始處包含下面的調用:_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );這條語句無論程序在什麼地方退出都會自動調用 _CrtDumpMemoryLeaks。注意:這裡必須同時設置兩個位域標志:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。
設置 CRT 報告模式
默認情況下,_CrtDumpMemoryLeaks 將內存洩漏信息 dump 到 Output 窗口的 Debug 頁, 如果你想將這個輸出定向到別的地方,可以使用 _CrtSetReportMode 進行重置。如果你使用某個庫,它可能將輸出定向到另一位置。此時,只要使用以下語句將輸出位置設回 Output 窗口即可:
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );
有關使用 _CrtSetReportMode 的詳細信息,請參考 MSDN 庫關於 _CrtSetReportMode 的描述。
二、解釋內存塊類型
前面已經說過,內存洩漏報告中把每一塊洩漏的內存分為 normal(普通塊)、client(客戶端塊)和 CRT 塊。事實上,需要留心和注意的也就是 normal 和 client,即普通塊和客戶端塊。
1.normal block(普通塊):這是由你的程序分配的內存。
2.client block(客戶塊):這是一種特殊類型的內存塊,專門用於 MFC 程序中需要析構函數的對象。MFC new 操作符視具體情況既可以為所創建的對象建立普通塊,也可以為之建立客戶塊。
3.CRT block(CRT 塊):是由 C RunTime Library 供自己使用而分配的內存塊。由 CRT 庫自己來管理這些內存的分配與釋放,我們一般不會在內存洩漏報告中發現 CRT 內存洩漏,除非程序發生了嚴重的錯誤(例如 CRT 庫崩潰)。
除了上述的類型外,還有下面這兩種類型的內存塊,它們不會出現在內存洩漏報告中:
1.free block(空閒塊):已經被釋放(free)的內存塊。
2.Ignore block(忽略塊):這是程序員顯式聲明過不要在內存洩漏報告中出現的內存塊。
三、如何在內存分配序號處設置斷點
在內存洩漏報告中,的文件名和行號可告訴分配洩漏的內存的代碼位置,但僅僅依賴這些信息來了解完整的洩漏原因是不夠的。因為一個程序在運行時,一段分配內存的代碼可能會被調用很多次,只要有一次調用後沒有釋放內存就會導致內存洩漏。為了確定是哪些內存沒有被釋放,不僅要知道洩漏的內存是在哪裡分配的,還要知道洩漏產生的條件。這時內存分配序號就顯得特別有用 ——這個序號就是文件名和行號之後的花括弧裡的那個數字。
例如,在本文例子代碼的輸出信息中,“45”是內存分配序號,意思是洩漏的內存是你程序中分配的第四十五個內存塊:
Detected memory leaks!
Dumping objects ->
C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
......
Object dump complete.
CRT 庫對程序運行期間分配的所有內存塊進行計數,包括由 CRT 庫自己分配的內存和其它庫(如 MFC)分配的內存。因此,分配序號為 N 的對象即為程序中分配的第 N 個對象,但不一定是代碼分配的第 N 個對象。(大多數情況下並非如此。)這樣的話,你便可以利用分配序號在分配內存的位置設置一個斷點。方法是在程序起始附近設置一個位置斷點。當程序在該點中斷時,可以從 QuickWatch(快速監視)對話框或 Watch(監視)窗口設置一個內存分配斷點:
例如,在 Watch 窗口中,在 Name 欄鍵入下面的表達式:
_crtBreakAlloc
如果要使用 CRT 庫的多線程 DLL 版本(/MD 選項),那麼必須包含上下文操作符,像這樣:
{,,msvcrtd.dll}_crtBreakAlloc
現在按下回車鍵,調試器將計算該值並把結果放入 Value 欄。如果沒有在內存分配點設置任何斷點,該值將為 –1。
用你想要在其位置中斷的內存分配的分配序號替換 Value 欄中的值。例如輸入 45。這樣就會在分配序號為 45 的地方中斷。
在所感興趣的內存分配處設置斷點後,可以繼續調試。這時,運行程序時一定要小心,要保證內存塊分配的順序不會改變。當程序在指定的內存分配處中斷時,可以查看 Call Stack(調用堆棧)窗口和其它調試器信息以確定分配內存時的情況。如果必要,可以從該點繼續執行程序,以查看對象發生了什麼情況,或許可以確定未正確釋放對象的原因。
盡管通常在調試器中設置內存分配斷點更方便,但如果願意,也可在代碼中設置這些斷點。為了在代碼中設置一個內存分配斷點,可以增加這樣一行(對於第四十五個內存分配):
_crtBreakAlloc = 45;
你還可以使用有相同效果的 _CrtSetBreakAlloc 函數:
_CrtSetBreakAlloc(45);
四、如何比較內存狀態
定位內存洩漏的另一個方法就是在關鍵點獲取應用程序內存狀態的快照。CRT 庫提供了一個結構類型 _CrtMemState。你可以用它來存儲內存狀態的快照:
_CrtMemState s1, s2, s3;
若要獲取給定點的內存狀態快照,可以向 _CrtMemCheckpoint 函數傳遞一個 _CrtMemState 結構。該函數用當前內存狀態的快照填充此結構:
_CrtMemCheckpoint( &s1 );
通過向 _CrtMemDumpStatistics 函數傳遞 _CrtMemState 結構,可以在任意地方 dump 該結構的內容:
_CrtMemDumpStatistics( &s1 );
該函數輸出如下格式的 dump 內存分配信息:
0 bytes in 0 Free Blocks.
75 bytes in 3 Normal Blocks.
5037 bytes in 41 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 5308 bytes.
Total allocations: 7559 bytes.
若要確定某段代碼中是否發生了內存洩漏,可以通過獲取該段代碼之前和之後的內存狀態快照,然後使用 _CrtMemDifference 比較這兩個狀態:
_CrtMemCheckpoint( &s1 );// 獲取第一個內存狀態快照
// 在這裡進行內存分配
_CrtMemCheckpoint( &s2 );// 獲取第二個內存狀態快照
// 比較兩個內存快照的差異
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );// dump 差異結果
顧名思義,_CrtMemDifference 比較兩個內存狀態(前兩個參數),生成這兩個狀態之間差異的結果(第三個參數)。在程序的開始和結尾放置 _CrtMemCheckpoint 調用,並使用 _CrtMemDifference 比較結果,是檢查內存洩漏的另一種方法。如果檢測到洩漏,則可以使用 _CrtMemCheckpoint 調用通過二進制搜索技術來分割程序和定位洩漏。
五、結論
盡管 VC ++ 具有一套專門調試 MFC 應用程序的機制,但本文上述討論的內存分配很簡單,沒有涉及到 MFC 對象,所以這些內容同樣也適用於 MFC 程序。在 MSDN 庫中可以找到很多有關 VC++ 調試方面的資料,如果你能善用 MSDN 庫,相信用不了多少時間你就有可能成為調試高手