您還未登錄!|登錄|注冊|幫助
CSDN首頁 資訊 論壇 博客 下載 搜索 更多CTO俱樂部
學生大本營
培訓充電
移動開發(fā)
軟件研發(fā)
云計算
程序員
TUP
bairny的專欄
條新通知 登錄 注冊 歡迎 退出 我的博客 配置 寫文章 文章管理 博客首頁 全站 當前博客 空間 博客 好友 相冊 留言 用戶操作
[留言] [發(fā)消息] [加為好友]
byID:bairny
共16105次訪問,排名11515,好友0人,關注者1人。
by的文章
原創(chuàng) 10 篇
翻譯 0 篇
轉載 11 篇
評論 5 篇
訂閱我的博客
[編輯]bairny的公告
[編輯]文章分類
PE文件格式
WTL
存檔
2007年08月(4)
2007年07月(3)
2007年05月(11)
2007年03月(3)
公告: 第三屆中國云計算大會 六折票價搶購中! [意見反饋][官方博客] VC++ 6.0 中如何使用 CRT 調試功能來檢測內存泄漏(轉) 收藏
VC++ 6.0 中如何使用 CRT 調試功能來檢測內存泄漏
作者:JerryZ
下載例子源代碼
最近看了周星星 Blog 中的一篇文章:“VC++6.0中內存泄漏檢測”,受益匪淺,便運行其例子代碼想看看 Output 窗口中的輸出結果,可惜怎么弄其輸出都不是預期的東西,郁悶了半天,便到水壇里找到周星星,請求他指點一、二,然而未果。沒有辦法,最后我一頭栽進 MSDN 庫狂搜了一把,功夫不負有心人,我搜出很多有關這方面的資料,沒過多久我便基本上就找到了答案......
首先,檢測內存泄漏的基本工具是調試器和 CRT 調試堆函數。為了使用調試堆函數,必須在要檢測內存泄漏和調試的程序中添加下面的語句:
#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h>
#include "debug_new.h" MSDN 如是說:“必須保證上面聲明的順序,如果改變了順序,可能不能正常工作。”至于這是為什么,我們不得而知。MS 的老大們經常這樣故弄玄虛。
針對非 MFC 程序,再加上周星星的頭文件:debug_new.h,當然如果不加這一句,也能檢測出內存泄漏,但是你無法確定在哪個源程序文件中發(fā)生泄漏。Output 輸出只告訴你在 crtsdb.h 中的某個地方有內存泄漏。我測試時 REG_DEBUG_NEW 沒有起作用。加不加這個宏都可以檢測出發(fā)生內存分配泄漏的文件。
其次,一旦添加了上面的聲明,你就可以通過在程序中加入下面的代碼來報告內存泄漏信息了:
_CrtDumpMemoryLeaks(); 這就這么簡單。我在周星星的例子代碼中加入這些機關后,在 VC++ 調試會話(按 F5 調試運行) Output 窗口的 Debug 頁便看到了預期的內存泄漏 dump。該 dump 形式如下:
Detected memory leaks!
Dumping objects ->
c:\Program Files\...\include\crtdbg.h(552) : {45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
c:\Program Files\...\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\...\include\crtdbg.h(552) : {43} normal block at 0x00441C20, 40 bytes long.
Data: < C > E8 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00
Object dump complete. 更具體的細節(jié)請參考本文附帶的源代碼文件。
下面是我看過 MSDN 資料后,針對“如何使用 CRT 調試功能來檢測內存泄漏?”的問題進行了一番編譯和整理,希望對大家有用。如果你的英文很棒,那就不用往下看了,建議直接去讀 MSDN 庫中的技術原文。
C/C++ 編程語言的最強大功能之一便是其動態(tài)分配和釋放內存,但是中國有句古話:“最大的長處也可能成為最大的弱點”,那么 C/C++ 應用程序正好印證了這句話。在 C/C++ 應用程序開發(fā)過程中,動態(tài)分配的內存處理不當是最常見的問題。其中,最難捉摸也最難檢測的錯誤之一就是內存泄漏,即未能正確釋放以前分配的內存的錯誤。偶爾發(fā)生的少量內存泄漏可能不會引起我們的注意,但泄漏大量內存的程序或泄漏日益增多的程序可能會表現出各種 各樣的征兆:從性能不良(并且逐漸降低)到內存完全耗盡。更糟的是,泄漏的程序可能會用掉太多內存,導致另外一個程序垮掉,而使用戶無從查找問題的真正根源。此外,即使無害的內存泄漏也可能殃及池魚。
幸運的是,Visual Studio 調試器和 C 運行時 (CRT) 庫為我們提供了檢測和識別內存泄漏的有效方法。下面請和我一起分享收獲——如何使用 CRT 調試功能來檢測內存泄漏?
如何啟用內存泄漏檢測機制?
使用 _CrtSetDbgFlag
設置 CRT 報告模式
解釋內存塊類型
如何在內存分配序號處設置斷點?
如何比較內存狀態(tài)?
結論
如何啟用內存泄漏檢測機制?
VC++ IDE 的默認狀態(tài)是沒有啟用內存泄漏檢測機制的,也就是說即使某段代碼有內存泄漏,調試會話的 Output 窗口的 Debug 頁不會輸出有關內存泄漏信息。你必須設定兩個最基本的機關來啟用內存泄漏檢測機制。
一是使用調試堆函數:
#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h> 注意:#include 語句的順序。如果更改此順序,所使用的函數可能無法正確工作。
通過包含 crtdbg.h 頭文件,可以將 malloc 和 free 函數映射到其“調試”版本 _malloc_dbg 和 _free_dbg,這些函數會跟蹤內存分配和釋放。此映射只在調試(Debug)版本(也就是要定義 _DEBUG)中有效。發(fā)行版本(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. 根據這段輸出信息,你無法知道在哪個源程序文件里發(fā)生了內存泄漏。下面我們來研究一下輸出信息的格式。第一行和第二行沒有什么可說的,從第三行開始:
xx}:花括弧內的數字是內存分配序號,本文例子中是 {45},{44},{43};
block:內存塊的類型,常用的有三種:normal(普通)、client(客戶端)或 CRT(運行時);本文例子中是:normal block;
用十六進制格式表示的內存位置,如:at 0x00441BA0 等;
以字節(jié)為單位表示的內存塊的大小,如:32 bytes long;
前 16 字節(jié)的內容(也是用十六進制格式表示),如:Data: <AB> 41 42 等; 仔細觀察不難發(fā)現,如果定義了 _CRTDBG_MAP_ALLOC ,那么在內存分配序號前面還會顯示在其中分配泄漏內存的文件名,以及文件名后括號中的數字表示發(fā)生泄漏的代碼行號,比如:
C:\Temp\memleak\memleak.cpp(15) 雙擊 Output 窗口中此文件名所在的輸出行,便可跳到源程序文件分配該內存的代碼行(也可以選中該行,然后按 F4,效果一樣) ,這樣一來我們就很容易定位內存泄漏是在哪里發(fā)生的了,因此,_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,即普通塊和客戶端塊。
normal block(普通塊):這是由你的程序分配的內存。
client block(客戶塊):這是一種特殊類型的內存塊,專門用于 MFC 程序中需要析構函數的對象。MFC new 操作符視具體情況既可以為所創(chuàng)建的對象建立普通塊,也可以為之建立客戶塊。
CRT block(CRT 塊):是由 C RunTime Library 供自己使用而分配的內存塊。由 CRT 庫自己來管理這些內存的分配與釋放,我們一般不會在內存泄漏報告中發(fā)現 CRT 內存泄漏,除非程序發(fā)生了嚴重的錯誤(例如 CRT 庫崩潰)。
除了上述的類型外,還有下面這兩種類型的內存塊,它們不會出現在內存泄漏報告中:
free block(空閑塊):已經被釋放(free)的內存塊。
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(快速監(jiān)視)對話框或 Watch(監(jiān)視)窗口設置一個內存分配斷點:
例如,在 Watch 窗口中,在 Name 欄鍵入下面的表達式:
_crtBreakAlloc如果要使用 CRT 庫的多線程 DLL 版本(/MD 選項),那么必須包含上下文操作符,像這樣:
{,,msvcrtd.dll}_crtBreakAlloc 現在按下回車鍵,調試器將計算該值并把結果放入 Value 欄。如果沒有在內存分配點設置任何斷點,該值將為 –1。
用你想要在其位置中斷的內存分配的分配序號替換 Value 欄中的值。例如輸入 45。這樣就會在分配序號為 45 的地方中斷。
在所感興趣的內存分配處設置斷點后,可以繼續(xù)調試。這時,運行程序時一定要小心,要保證內存塊分配的順序不會改變。當程序在指定的內存分配處中斷時,可以查看 Call Stack(調用堆棧)窗口和其它調試器信息以確定分配內存時的情況。如果必要,可以從該點繼續(xù)執(zhí)行程序,以查看對象發(fā)生了什么情況,或許可以確定未正確釋放對象的原因。
盡管通常在調試器中設置內存分配斷點更方便,但如果愿意,也可在代碼中設置這些斷點。為了在代碼中設置一個內存分配斷點,可以增加這樣一行(對于第四十五個內存分配):
_crtBreakAlloc = 45;你還可以使用有相同效果的 _CrtSetBreakAlloc 函數:
_CrtSetBreakAlloc(45);如何比較內存狀態(tài)?
定位內存泄漏的另一個方法就是在關鍵點獲取應用程序內存狀態(tài)的快照。CRT 庫提供了一個結構類型 _CrtMemState。你可以用它來存儲內存狀態(tài)的快照:
_CrtMemState s1, s2, s3; 若要獲取給定點的內存狀態(tài)快照,可以向 _CrtMemCheckpoint 函數傳遞一個 _CrtMemState 結構。該函數用當前內存狀態(tài)的快照填充此結構:
_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. 若要確定某段代碼中是否發(fā)生了內存泄漏,可以通過獲取該段代碼之前和之后的內存狀態(tài)快照,然后使用 _CrtMemDifference 比較這兩個狀態(tài):
_CrtMemCheckpoint( &s1 );// 獲取第一個內存狀態(tài)快照
// 在這里進行內存分配
_CrtMemCheckpoint( &s2 );// 獲取第二個內存狀態(tài)快照
// 比較兩個內存快照的差異
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );// dump 差異結果 顧名思義,_CrtMemDifference 比較兩個內存狀態(tài)(前兩個參數),生成這兩個狀態(tài)之間差異的結果(第三個參數)。在程序的開始和結尾放置 _CrtMemCheckpoint 調用,并使用 _CrtMemDifference 比較結果,是檢查內存泄漏的另一種方法。如果檢測到泄漏,則可以使用 _CrtMemCheckpoint 調用通過二進制搜索技術來分割程序和定位泄漏。
結論
盡管 VC ++ 具有一套專門調試 MFC 應用程序的機制,但本文上述討論的內存分配很簡單,沒有涉及到 MFC 對象,所以這些內容同樣也適用于 MFC 程序。在 MSDN 庫中可以找到很多有關 VC++ 調試方面的資料,如果你能善用 MSDN 庫,相信用不了多少時間你就有可能成為調試高手。
本人水平不高,謬誤在所難免,請大家拍磚,不要客氣。順祝大家圣誕快樂!
JerryZ 于 2004 年平安夜,
調試方法和技巧
作者:非凡
便于調試的代碼風格:
不用全局變量
所有變量都要初始化,成員變量在構造函數中初始化
盡量使用const
詳盡的注釋
VC++編譯選項:
總是使用/W4警告級別
在調試版本里總是使用/GZ編譯選項,用來發(fā)現在Release版本中才有的錯誤
沒有警告的編譯:保證在編譯后沒有任何警告,但是在消除警告前要進行仔細檢查
調試方法:
1、使用 Assert(原則:盡量簡單)
assert只在debug下生效,release下不會被編譯。
例子:
char* strcpy(char* dest,char* source)
{
assert(source!=0);
assert(dest!=0);
char* returnstring = dest;
while((*dest++ = *source++)!= ‘\0’)
{
;
}
return returnstring;
} 2、防御性的編程
例子:
char* strcpy(char* dest,char* source)
{
if(source == 0)
{
assert(false);
reutrn 0;
}
if(dest == 0)
{
assert(false);
return 0;
}
char* returnstring = dest;
while((*dest++ = *source++)!= ‘\0’)
{
;
}
return returnstring;
} 3、使用Trace
以下的例子只能在debug中顯示,
例子:
a)、TRACE
CString csTest = “test”;
TRACE(“CString is %s\n”,csTest);b)、ATLTRACE
c)、afxDump
CTime time = CTime::GetCurrentTime();
#ifdef _DEBUG
afxDump << time << “\n”;
#endif4、用GetLastError來檢測返回值,通過得到錯誤代碼來分析錯誤原因
5、把錯誤信息記錄到文件中
異常處理
程序設計時一定要考慮到異常如何處理,當錯誤發(fā)生后,不應簡單的報告錯誤并退出程序,應當盡可能的想辦法恢復到出錯前的狀態(tài)或者讓程序從頭開始運行,并且對于某些錯誤,應該能夠容錯,即允許錯誤的存在,但是程序還是能夠正常完成任務。
調試技巧
1、VC++中F5進行調試運行
a)、在output Debug窗口中可以看到用TRACE打印的信息
b)、 Call Stack窗口中能看到程序的調用堆棧
2、當Debug版本運行時發(fā)生崩潰,選擇retry進行調試,通過看Call Stack分析出錯的位置及原因
3、使用映射文件調試
a)、創(chuàng)建映射文件:Project settings中l(wèi)ink項,選中Generate mapfile,輸出程序代碼地址:/MAPINFO: LINES,得到引出序號:/MAPINFO: EXPORTS。
b)、程序發(fā)布時,應該把所有模塊的映射文件都存檔。
c)、查看映射文件:見” 通過崩潰地址找出源代碼的出錯行”文件。
4、可以調試的Release版本
Project settings中C++項的Debug Info選擇為Program Database,Link項的Debug中選擇Debug Info和Microsoft format。
5、查看API的錯誤碼,在watch窗口輸入@err可以查看或者@err,hr,其中”,hr”表示錯誤碼的說明。
6、Set Next Statement:該功能可以直接跳轉到指定的代碼行執(zhí)行,一般用來測試異常處理的代碼。
7、調試內存變量的變化:當內存發(fā)生變化時停下來。
常見錯誤
1、在函數返回的時候程序崩潰的原因
a)、寫自動變量越界
b)、函數原型不匹配
2、MFC
a)、使用錯誤的函數原型處理用戶定義消息
正確的函數原型為:
afx_msg LRESULT OnMyMessage(WPARAM wParam,LPARAM lParam);3、謹慎使用TerminateThread:使用TerminateThread會造成資源泄漏,不到萬不得已,不要使用。
4、使用_beginthreadex,不要使用Create Thread來常見線程。
參考資料:
《Windows程序調試》
功能強大的vc6調試器
作者:yy2better
要成為一位優(yōu)秀的軟件工程師,調試能力必不可缺。本文將較詳細介紹VC6調試器的主要用法。
windows平臺的調試器主要分為兩大類:
1 用戶模式(user-mode)調試器:它們都基于win32 Debugging API,有使用方便的界面,主要用于調試用戶模式下的應用程序。這類調試器包括Visual C++調試器、WinDBG、BoundChecker、Borland C++ Builder調試器、NTSD等。
2 內核模式(kernel-mode)調試器:內核調試器位于CPU和操作系統(tǒng)之間,一旦啟動,操作系統(tǒng)也會中止運行,主要用于調試驅動程序或用戶模式調試器不易調試的程序。這類調試器包括WDEB386、WinDBG和softice等。其中WinDBG和softice也可以調試用戶模式代碼。
國外一位調試高手曾說,他70%調試時間是在用VC++,其余時間是使用WinDBG和softice。畢竟,調試用戶模式代碼,VC6調試器的效率是非常高的。因此,我將首先在本篇介紹VC6調試器的主要用法,其他調試器的用法及一些調試技能在后續(xù)文章中闡述。
一 位置斷點(Location Breakpoint)
大家最常用的斷點是普通的位置斷點,在源程序的某一行按F9就設置了一個位置斷點。但對于很多問題,這種樸素的斷點作用有限。譬如下面這段代碼:
void CForDebugDlg::OnOK()
{
for (int i = 0; i < 1000; i++) //A
{
int k = i * 10 - 2; //B
SendTo(k); //C
int tmp = DoSome(i); //D
int j = i / tmp; //E
}
}
執(zhí)行此函數,程序崩潰于E行,發(fā)現此時tmp為0,假設tmp本不應該為0,怎么這個時候為0呢?所以最好能夠跟蹤此次循環(huán)時DoSome函數是如何運行的,但由于是在循環(huán)體內,如果在E行設置斷點,可能需要按F5(GO)許多次。這樣手要不停的按,很痛苦。使用VC6斷點修飾條件就可以輕易解決此問題。步驟如下。
1 Ctrl+B打開斷點設置框,如下圖:
Figure 1設置高級位置斷點
2 然后選擇D行所在的斷點,然后點擊condition按鈕,在彈出對話框的最下面一個編輯框中輸入一個很大數目,具體視應用而定,這里1000就夠了。
3 按F5重新運行程序,程序中斷。Ctrl+B打開斷點框,發(fā)現此斷點后跟隨一串說明:...487 times remaining。意思是還剩下487次沒有執(zhí)行,那就是說執(zhí)行到513(1000-487)次時候出錯的。因此,我們按步驟2所講,更改此斷點的skip次數,將1000改為513。
4 再次重新運行程序,程序執(zhí)行了513次循環(huán),然后自動停在斷點處。這時,我們就可以仔細查看DoSome是如何返回0的。這樣,你就避免了手指的痛苦,節(jié)省了時間。
再看位置斷點其他修飾條件。如Figure 1所示,在“Enter the expression to be evaluated:”下面,可以輸入一些條件,當這些條件滿足時,斷點才啟動。譬如,剛才的程序,我們需要i為100時程序停下來,我們就可以輸入在編輯框中輸入“i==100”。
另外,如果在此編輯框中如果只輸入變量名稱,則變量發(fā)生改變時,斷點才會啟動。這對檢測一個變量何時被修改很方便,特別對一些大程序。
用好位置斷點的修飾條件,可以大大方便解決某些問題。
二 數據斷點(Data Breakpoint)
軟件調試過程中,有時會發(fā)現一些數據會莫名其妙的被修改掉(如一些數組的越界寫導致覆蓋了另外的變量),找出何處代碼導致這塊內存被更改是一件棘手的事情(如果沒有調試器的幫助)。恰當運用數據斷點可以快速幫你定位何時何處這個數據被修改。譬如下面一段程序:
#include "stdafx.h"
#include
int main(int argc, char* argv[])
{
char szName1[10];
char szName2[4];
strcpy(szName1,"shenzhen");
printf("%s\n", szName1); //A
strcpy(szName2, "vckbase"); //B
printf("%s\n", szName1);
printf("%s\n", szName2);
return 0;
}
這段程序的輸出是
szName1: shenzhen
szName1: ase
szName2: vckbase
szName1何時被修改呢?因為沒有明顯的修改szName1代碼。我們可以首先在A行設置普通斷點,F5運行程序,程序停在A行。然后我們再設置一個數據斷點。如下圖:
Figure 2 數據斷點
F5繼續(xù)運行,程序停在B行,說明B處代碼修改了szName1。B處明明沒有修改szName1呀?但調試器指明是這一行,一般不會錯,所以還是靜下心來看看程序,哦,你發(fā)現了:szName2只有4個字節(jié),而strcpy了7個字節(jié),所以覆寫了szName1。
數據斷點不只是對變量改變有效,還可以設置變量是否等于某個值。譬如,你可以將Figure 2中紅圈處改為條件”szName2[0]==''''y''''“,那么當szName2第一個字符為y時斷點就會啟動。
可以看出,數據斷點相對位置斷點一個很大的區(qū)別是不用明確指明在哪一行代碼設置斷點。
三 其他
1 在call stack窗口中設置斷點,選擇某個函數,按F9設置一個斷點。這樣可以從深層次的函數調用中迅速返回到需要的函數。
2 Set Next StateMent命令(debug過程中,右鍵菜單中的命令)
此命令的作用是將程序的指令指針(EIP)指向不同的代碼行。譬如,你正在調試上面那段代碼,運行在A行,但你不愿意運行B行和C行代碼,這時,你就可以在D行,右鍵,然后“Set Next StateMent”。調試器就不會執(zhí)行B、C行。只要在同一函數內,此指令就可以隨意跳前或跳后執(zhí)行。靈活使用此功能可以大量節(jié)省調試時間。
3 watch窗口
watch窗口支持豐富的數據格式化功能。如輸入0x65,u,則在右欄顯示101。
實時顯示windows API調用的錯誤:在左欄輸入@err,hr。
在watch窗口中調用函數。提醒一下,調用完函數后馬上在watch窗口中清除它,否則,單步調試時每一步調試器都會調用此函數。
4 messages斷點不怎么實用?;旧峡梢杂们懊嬷v述的斷點代替。
總結
調試最重要的還是你要思考,要猜測你的程序可能出錯的地方,然后運用你的調試器來證實你的猜測。而熟練使用上面這些技巧無疑會加快這個過程。最后,大家如果有關于調試方面的問題,我樂意參與探討。
VC調試入門
作者:阿榮
概述
調試是一個程序員最基本的技能,其重要性甚至超過學習一門語言。不會調試的程序員就意味著他即使會一門語言,卻不能編制出任何好的軟件。
這里我簡要的根據自己的經驗列出調試中比較常用的技巧,希望對大家有用。
本文約定,在選擇菜單時,通過/表示分級菜單,例如File/Open表示頂級菜單File的子菜單Open。
設置
為了調試一個程序,首先必須使程序中包含調試信息。一般情況下,一個從AppWizard創(chuàng)建的工程中包含的Debug Configuration自動包含調試信息,但是是不是Debug版本并不是程序包含調試信息的決定因素,程序設計者可以在任意的Configuration中增加調試信息,包括Release版本。
為了增加調試信息,可以按照下述步驟進行:
打開Project settings對話框(可以通過快捷鍵ALT+F7打開,也可以通過IDE菜單Project/Settings打開)
選擇C/C++頁,Category中選擇general ,則出現一個Debug Info下拉列表框,可供選擇的調試信息 方式包括:
命令行 Project settings 說明
無 None 沒有調試信息
/Zd Line Numbers Only 目標文件或者可執(zhí)行文件中只包含全局和導出符號以及代碼行信息,不包含符號調試信息
/Z7 C 7.0- Compatible 目標文件或者可執(zhí)行文件中包含行號和所有符號調試信息,包括變量名及類型,函數及原型等
/Zi Program Database 創(chuàng)建一個程序庫(PDB),包括類型信息和符號調試信息。
/ZI Program Database for Edit and Continue 除了前面/Zi的功能外,這個選項允許對代碼進行調試過程中的修改和繼續(xù)執(zhí)行。這個選項同時使#pragma設置的優(yōu)化功能無效
選擇Link頁,選中復選框"Generate Debug Info",這個選項將使連接器把調試信息寫進可執(zhí)行文件和DLL
如果C/C++頁中設置了Program Database以上的選項,則Link incrementally可以選擇。選中這個選項,將使程序可以在上一次編譯的基礎上被編譯(即增量編譯),而不必每次都從頭開始編譯。
斷點
斷點是調試器設置的一個代碼位置。當程序運行到斷點時,程序中斷執(zhí)行,回到調試器。斷點是 最常用的技巧。調試時,只有設置了斷點并使程序回到調試器,才能對程序進行在線調試。
設置斷點:可以通過下述方法設置一個斷點。首先把光標移動到需要設置斷點的代碼行上,然后
按F9快捷鍵
彈出Breakpoints對話框,方法是按快捷鍵CTRL+B或ALT+F9,或者通過菜單Edit/Breakpoints打開。打開后點擊Break at編輯框的右側的箭頭,選擇 合適的位置信息。一般情況下,直接選擇line xxx就足夠了,如果想設置不是當前位置的斷點,可以選擇Advanced,然后填寫函數、行號和可執(zhí)行文件信息。
去掉斷點:把光標移動到給定斷點所在的行,再次按F9就可以取消斷點。同前面所述,打開Breakpoints對話框后,也可以按照界面提示去掉斷點。
條件斷點:可以為斷點設置一個條件,這樣的斷點稱為條件斷點。對于新加的斷點,可以單擊Conditions按鈕,為斷點設置一個表達式。當這個表達式發(fā)生改變時,程序就 被中斷。底下設置包括“觀察數組或者結構的元素個數”,似乎可以設置一個指針所指向的內存區(qū)的大小,但是我設置一個比較的值但是改動 范圍之外的內存區(qū)似乎也導致斷點起效。最后一個設置可以讓程序先執(zhí)行多少次然后才到達斷點。
數據斷點:數據斷點只能在Breakpoints對話框中設置。選擇“Data”頁,就顯示了設置數據斷點的對話框。在編輯框中輸入一個表達式,當這個 表達式的值發(fā)生變化時,數據斷點就到達。一般情況下,這個表達式應該由運算符和全局變量構成,例如:在編輯框中輸入 g_bFlag這個全局變量的名字,那么當程序中有g_bFlag= !g_bFlag時,程序就將停在這個語句處。
消息斷點:VC也支持對Windows消息進行截獲。他有兩種方式進行截獲:窗口消息處理函數和特定消息中斷。
在Breakpoints對話框中選擇Messages頁,就可以設置消息斷點。如果在上面那個對話框中寫入消息處理函數的名字,那么 每次消息被這個函數處理,斷點就到達(我覺得如果采用普通斷點在這個函數中截獲,效果應該一樣)。如果在底下的下拉 列表框選擇一個消息,則每次這種消息到達,程序就中斷。
值
Watch
VC支持查看變量、表達式和內存的值。所有這些觀察都必須是在斷點中斷的情況下進行。
觀看變量的值最簡單,當斷點到達時,把光標移動到這個變量上,停留一會就可以看到變量的值。
VC提供一種被成為Watch的機制來觀看變量和表達式的值。在斷點狀態(tài)下,在變量上單擊右鍵,選擇Quick Watch, 就彈出一個對話框,顯示這個變量的值。
單擊Debug工具條上的Watch按鈕,就出現一個Watch視圖(Watch1,Watch2,Watch3,Watch4),在該視圖中輸入變量或者表達式,就可以觀察 變量或者表達式的值。注意:這個表達式不能有副作用,例如++運算符絕對禁止用于這個表達式中,因為這個運算符將修改變量的值,導致 軟件的邏輯被破壞。
Memory
由于指針指向的數組,Watch只能顯示第一個元素的值。為了顯示數組的后續(xù)內容,或者要顯示一片內存的內容,可以使用memory功能。在 Debug工具條上點memory按鈕,就彈出一個對話框,在其中輸入地址,就可以顯示該地址指向的內存的內容。
Varibles
Debug工具條上的Varibles按鈕彈出一個框,顯示所有當前執(zhí)行上下文中可見的變量的值。特別是當前指令涉及的變量,以紅色顯示。
寄存器
Debug工具條上的Reigsters按鈕彈出一個框,顯示當前的所有寄存器的值。
進程控制
VC允許被中斷的程序繼續(xù)運行、單步運行和運行到指定光標處,分別對應快捷鍵F5、F10/F11和CTRL+F10。各個快捷鍵功能如下:
快捷鍵 說明
F5 繼續(xù)運行
F10 單步,如果涉及到子函數,不進入子函數內部
F11 單步,如果涉及到子函數,進入子函數內部
CTRL+F10 運行到當前光標處。
Call Stack
調用堆棧反映了當前斷點處函數是被那些函數按照什么順序調用的。單擊Debug工具條上的Call stack就顯示Call Stack對話框。在CallStack對話框中顯示了一個調用系列,最上面的是當前函數,往下依次是調用函數的上級函數。單擊這些函數名可以跳到對應的函數中去。
其他調試手段
系統(tǒng)提供一系列特殊的函數或者宏來處理Debug版本相關的信息,如下:
宏名/函數名 說明
TRACE 使用方法和printf完全一致,他在output框中輸出調試信息
ASSERT 它接收一個表達式,如果這個表達式為TRUE,則無動作,否則中斷當前程序執(zhí)行。對于系統(tǒng)中出現這個宏 導致的中斷,應該認為你的函數調用未能滿足系統(tǒng)的調用此函數的前提條件。例如,對于一個還沒有創(chuàng)建的窗口調用SetWindowText等。
VERIFY 和ASSERT功能類似,所不同的是,在Release版本中,ASSERT不計算輸入的表達式的值,而VERIFY計算表達式的值。
關注
一個好的程序員不應該把所有的判斷交給編譯器和調試器,應該在程序中自己加以程序保護和錯誤定位,具體措施包括:
對于所有有返回值的函數,都應該檢查返回值,除非你確信這個函數調用絕對不會出錯,或者不關心它是否出錯。
一些函數返回錯誤,需要用其他函數獲得錯誤的具體信息。例如accept返回INVALID_SOCKET表示accept失敗,為了查明 具體的失敗原因,應該立刻用WSAGetLastError獲得錯誤碼,并針對性的解決問題。
有些函數通過異常機制拋出錯誤,應該用TRY-CATCH語句來檢查錯誤
程序員對于能處理的錯誤,應該自己在底層處理,對于不能處理的,應該報告給用戶讓他們決定怎么處理。如果程序出了異常, 卻不對返回值和其他機制返回的錯誤信息進行判斷,只能是加大了找錯誤的難度。
另外:VC中要編制程序不應該一開始就寫cpp/h文件,而應該首先創(chuàng)建一個合適的工程。因為只有這樣,VC才能選擇合適的編譯、連接 選項。對于加入到工程中的cpp文件,應該檢查是否在第一行顯式的包含stdafx.h頭文件,這是Microsoft Visual Studio為了加快編譯 速度而設置的預編譯頭文件。在這個#include "stdafx.h"行前面的所有代碼將被忽略,所以其他頭文件應該在這一行后面被包含。
對于.c文件,由于不能包含stdafx.h,因此可以通過Project settings把它的預編譯頭設置為“不使用”,方法是:
彈出Project settings對話框
選擇C/C++
Category選擇Precompilation Header
選擇不使用預編譯頭。
關于調試時輸出的字符串信息
作者:①塌糊涂
下載源代碼
使用工具:VC6.0,IDA
當我們要在程序中輸出調試信息時,常常以字符串的形式來輸出,例如:
printf("Some debug information here!\n");這段代碼在Debug和Release版下都輸出調試信息,這不是我們所要的,一般地大家都會添加
預編譯指令,如下所示:
#if _DEBUG
printf("Some debug information here!\n");
#endif這樣就達到了在Debug版里程序輸出調試信息,在Release版下不輸出調試信息的目的。(在Release版里
連printf函數都沒有調用)可如果要在程序里的許多地方輸出調試信息,若采用上面的方式會很麻煩;
(至于為什么麻煩,可能就是不愿多敲幾次鍵盤吧,呵呵。。。)
于是大家都想到寫個輸出函數,代碼如下:
void printInfo(char *strInfo)
{
#if _DEBUG
printf(strInfo);
#endif
}注:該函數只是演示用的,很簡單,沒有其他檢查字符串功能。
在要輸出調試信息的地方,調用如下語句就行:
printInfo("Some debug information here!\n");
確實,在Debug模式下運行該程序,則輸出如下信息:
Some debug information here!在Release模式下,則沒輸出什么信息;
我們往往在這個時候認為一切都OK了;如果你認為是,就沒必要往下看了;呵呵。。。
雖然在Release版下運行程序沒有輸出調試信息來,可這些調試信息卻留在了二進制的可執(zhí)行文件里;
我們可以用IDA來打開該Release版的可執(zhí)行文件,看到如圖一所示的信息:
圖一:IDA反匯編后的main函數
注:該函數就是main函數
可見調試信息字符串(“Some debug information here!\n”)確實存在于Release版的可執(zhí)行文件里;
我們當然不希望別人看到這些調試信息,那有沒有辦法來防止該調試信息被編譯進Release版的可執(zhí)行文件里呢?
辦法是有的,這里來描述2個方法。
辦法一:
定義如下宏:
#if _DEBUG
#define _D(str) str
#else
#define _D(str) NULL
#endif此時輸出語句變?yōu)椋?/p>
printInfo(_D("Some debug information here!\n"));
在Debug模式下運行程序,依然輸出調試信息:
“Some debug information here!”;在Release下,則什么都不輸出,此時我們用IDA看一下Release版的二進制文件,則沒有發(fā)現該調試信息字符串。
如圖二示:
圖二:IDA反匯編后的main函數
方法二:
定義如下宏:
#if _DEBUG
void printInfo(char *strInfo)
{
printf(strInfo);
}
#else
#define printInfo(str)
#endif注意:該宏把函數printInfo的定義也放進去了;
在Debug模式下運行程序,也同樣輸出調試信息:
“Some debug information here!”;在Release下,也什么都不輸出,此時我們用IDA看一下Release版的二進制文件,也沒有發(fā)現該調試信息字符串。
如圖三示:
圖三:IDA反匯編后的main函數
既然方法一和方法二都能實現同樣的功能,那究竟那個方法好呢?
方法一和方法二確實都沒在可執(zhí)行文件里留下調試信息,比較一下圖二和圖三,我們不難發(fā)現:
圖二當中多了一個函數調用 call nullsub_1,該函數就是printInfo,雖然該函數什么都不做,
但它卻調用了,我們一般也不希望該函數調用,所以方法一中多了一個函數調用,增加了開銷,
而方法二當中卻沒有調用該函數。
個人認為方法二較好。
結束語:
若要轉載該文章,請保持原文章的完整性,謝謝!
文中如有不妥之處,請指正,謝謝!
E-mail:grapeky@etang.com
調用規(guī)范與可變參數表
作者:阿半
語言調用規(guī)范是指進行一次函數調用所采用的傳遞參數的方法,返回值的處理以及調用堆棧的清理。Microsoft C/C++ 語言中采用了五種調用規(guī)范,分別是__cdecl, __stdcall, __fastcall,thiscall和nake每一中調用規(guī)范都是利用eax作為返回值,如果函數返回值是64位的,則利用edx:eax對來返回值。Nake調用規(guī)范非常的靈活,足以獨立的一篇文章描述,這里就不再描述nake調用規(guī)范。下表列出了前面四種規(guī)范調用的特點:
關鍵字 堆棧清理者 參數傳遞順序
__cdecl 調用者 從右至左
__stdcall 被調用者 從右至左
__fastcall 被調用者 從右至左,前兩個參數由寄存器ecx,edx傳遞
thiscall 被調用者或者調用者 從右至左
__cdecl 最大好處在于由于是調用者清理棧,它可以處理可變參數,缺點則在于它增加了程序的大小,因為在每個調用返回的時候,需要多執(zhí)行一條清理棧的指令。
__stdcall 是在windows程序設計中出現的最多的調用規(guī)則,所有的不可變參數的API調用都使用這個規(guī)則。
__fastcall 在windows內核設計中被廣泛的使用,由于兩個參數由寄存器直接傳遞,采用這種規(guī)則的函數效率要比以上兩種規(guī)則高。
thiscall是C++成員函數的默認調用規(guī)范,編譯期間,這種調用會根據函數是否支持可變參數表來決定采用什么方式清理堆棧。如果成員函數不支持可變參數,那么它就是用參數入棧,ecx保存this指針的方式進行調用,如果成員函數支持可變參數,那么它的調用和__cdecl類似,唯一不同的是將this指針最后壓入棧中進行傳遞。
調用者和被調用者必須采用同樣的規(guī)則才能保證程序的正常執(zhí)行,曾經看到很多程序員犯的錯誤就是由于調用規(guī)范的不一樣,致使程序異常,比如:
DWORD ThreadFunc(LPVOID lpParam)
{
//…
}
CreateThread(..,(LPTHREAD_START_ROUTINE)ThreadFunc, …); 如果在編譯期間沒有指定編譯選項/Gz(指定未指明調用規(guī)范的函數采用__stdcall方式),那么編譯器自動將ThreadFunc處理成__cdecl調用規(guī)范(/Gd),這樣可能在線程開始的時候正常執(zhí)行,然而退出的時候由于堆棧沒有正常清理,造成訪問違例或者非法指令錯誤。
以上說了很多清理棧的問題,那么為什么清理棧很重要呢。堆棧是線程相關的,也就是說每一個線程含有一個堆棧,這個堆棧上保存了局部變量,調用返回地址等很多線程相關的數據,這也是為什么獨立運行的線程可以調用同樣一個函數而互不干擾的原因。堆棧的特點恐怕大家已經非常熟悉了,那么根據上面的每一種調用,我給出一個簡單的圖示來說明清理堆棧的重要性,以及為什么上面的例子代碼會出錯。
圖一 這是線程堆棧在運行的時候的樣子
調用前和后esp的差值中間包含了函數參數表,返回地址這樣的重要信息,舉個簡單的調用例子.假設有某個函數定義是這樣的:
Int __cdecl func(void* p);再假設esp調用函數前的數值為0x1234,那么在進入這個函數體內看到的堆棧是這樣的:
122C 1230 1234
Next p 這里的next指調用函數后的下一條指令的位置。調用函數的匯編碼:
Push p
Call func
Add esp,4 《--注意這里,由于是cdecl調用,需要調用者清棧。而一個__stdcall調用的匯編碼:
Push p
Call func 這里沒有了add esp,4這個指令,因為在func函數返回的時候自己將esp已經復原了。再來看剛才舉的錯誤的例子,由于強制轉換的作用,線程開始函數被設置成了stdcall調用,而實際的線程函數被編譯后,并沒有執(zhí)行堆棧的清理工作,線程函數返回的時候,由于堆棧的不正確,當然會發(fā)生錯誤。修改這個bug的方法只要在線程函數的定義前把__cdecl改成_stdcall即可。
有了上面的例子做基礎來理解可變參數表就簡單的多了,由于各種調用規(guī)范的限定,致使只有__cdecl調用規(guī)范可以采用可變參數表。先來看看可變參數表的定義(可以參考sdk目錄下src\crt\varargs.h):
typedef char *va_list;
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_dcl va_list va_alist;
#define va_start(ap) ap = (va_list)&va_alist
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ap = (va_list)0 va_list居然被定義成char* ?沒錯,這實際是用來定義了一個指針,指針的sizeof()就是操作系統(tǒng)可訪問的地址空間的大小,也就是CPU相關的字長。_INTSIZEOF宏很簡單,就是用來將數據以n的數據大小對齊。va_start宏有點模糊,可是如果你看懂了上面的堆棧數據結構,那么顯然它就是獲得最后一個固定參數的地址,也就是堆棧上的地址,va_arg先使得ap指向下一個參數,然后取得當前參數的值(注意,這個值正是堆棧上的值),va_end使得取參數過程結束。
這幾個宏完成的動作很簡單了,實際就是取得可變參數表在堆棧上的起始位置,然后根據參數類型,依次從堆棧上取出每一個參數。
本文簡單的介紹了微軟C/C++支持的調用類型,結合實例描述了規(guī)范的實際應用,最后根據CRT提供的源代碼分析了可變參數表的實現。
僅通過崩潰地址找出源代碼的出錯行
作者:老羅
提交者:eastvc 發(fā)布日期:2003-10-23 9:16:11
原文出處:http://www.luocong.com/articles/show_article.asp?Article_ID=29
作為程序員,我們平時最擔心見到的事情是什么?是內存泄漏?是界面不好看?……錯啦!我相信我的看法是不會有人反對的——那就是,程序發(fā)生了崩潰!
“該程序執(zhí)行了非法操作,即將關閉。請與你的軟件供應商聯系。”,呵呵,這句 M$ 的“名言”,恐怕就是程序員最擔心見到的東西了。有的時候,自己的程序在自己的機器上運行得好好的,但是到了別人的機器上就崩潰了;有時自己在編寫和測試的過程中就莫名其妙地遇到了非法操作,但是卻無法確定到底是源代碼中的哪行引起的……是不是很痛苦呢?不要緊,本文可以幫助你走出這種困境,甚至你從此之后可以自豪地要求用戶把崩潰地址告訴你,然后你就可以精確地定位到源代碼中出錯的那行了。(很神奇吧?呵呵。)
首先我必須強調的是,本方法可以在目前市面上任意一款編譯器上面使用。但是我只熟悉 M$ 的 VC 和 MASM ,因此后面的部分只介紹如何在這兩個編譯器中實現,請讀者自行融會貫通,掌握在別的編譯器上使用的方法。
Well,廢話說完了,讓我們開始! :)
首先必須生成程序的 MAP 文件。什么是 MAP 文件?簡單地講, MAP 文件是程序的全局符號、源文件和代碼行號信息的唯一的文本表示方法,它可以在任何地方、任何時候使用,不需要有額外的程序進行支持。而且,這是唯一能找出程序崩潰的地方的救星。
好吧,既然 MAP 文件如此神奇,那么我們應該如何生成它呢?在 VC 中,我們可以按下 Alt+F7 ,打開“Project Settings”選項頁,選擇 C/C++ 選項卡,并在最下面的 Project Options 里面輸入:/Zd ,然后要選擇 Link 選項卡,在最下面的 Project Options 里面輸入: /mapinfo:lines 和 /map:PROJECT_NAME.map 。最后按下 F7 來編譯生成 EXE 可執(zhí)行文件和 MAP 文件。
在 MASM 中,我們要設置編譯和連接參數,我通常是這樣做的:
rc %1.rc
ml /c /coff /Zd %1.asm
link /subsystem:windows /mapinfo:exports /mapinfo:lines /map:%1.map %1.obj %1.res
把它保存成 makem.bat ,就可以在命令行輸入 makem filename 來編譯生成 EXE 可執(zhí)行文件和 MAP 文件了。
在此我先解釋一下加入的參數的含義:
/Zd 表示在編譯的時候生成行信息
/map[:filename] 表示生成 MAP 文件的路徑和文件名
/mapinfo:lines 表示生成 MAP 文件時,加入行信息
/mapinfo:exports 表示生成 MAP 文件時,加入 exported functions (如果生成的是 DLL 文件,這個選項就要加上)
OK,通過上面的步驟,我們已經得到了 MAP 文件,那么我們該如何利用它呢?
讓我們從簡單的實例入手,請打開你的 VC ,新建這樣一個文件:
01 //****************************************************************
02 //程序名稱:演示如何通過崩潰地址找出源代碼的出錯行
03 //作者:羅聰
04 //日期:2003-2-7
05 //出處:http://www.luocong.com(老羅的繽紛天地)
06 //本程序會產生“除0錯誤”,以至于會彈出“非法操作”對話框。
07 //“除0錯誤”只會在 Debug 版本下產生,本程序為了演示而盡量簡化。
08 //注意事項:如欲轉載,請保持本程序的完整,并注明:
09 //轉載自“老羅的繽紛天地”(http://www.luocong.com)
10 //****************************************************************
11
12 void Crash(void)
13 {
14 int i = 1;
15 int j = 0;
16 i /= j;
17 }
18
19 void main(void)
20 {
21 Crash();
22 }
很顯然本程序有“除0錯誤”,在 Debug 方式下編譯的話,運行時肯定會產生“非法操作”。好,讓我們運行它,果然,“非法操作”對話框出現了,這時我們點擊“詳細信息”按鈕,記錄下產生崩潰的地址——在我的機器上是 0x0040104a 。
再看看它的 MAP 文件:(由于文件內容太長,中間沒用的部分我進行了省略)
CrashDemo
Timestamp is 3e430a76 (Fri Feb 07 09:23:02 2003)
Preferred load address is 00400000
Start Length Name Class
0001:00000000 0000de04H .text CODE
0001:0000de04 0001000cH .textbss CODE
0002:00000000 00001346H .rdata DATA
0002:00001346 00000000H .edata DATA
0003:00000000 00000104H .CRT$XCA DATA
0003:00000104 00000104H .CRT$XCZ DATA
0003:00000208 00000104H .CRT$XIA DATA
0003:0000030c 00000109H .CRT$XIC DATA
0003:00000418 00000104H .CRT$XIZ DATA
0003:0000051c 00000104H .CRT$XPA DATA
0003:00000620 00000104H .CRT$XPX DATA
0003:00000724 00000104H .CRT$XPZ DATA
0003:00000828 00000104H .CRT$XTA DATA
0003:0000092c 00000104H .CRT$XTZ DATA
0003:00000a30 00000b93H .data DATA
0003:000015c4 00001974H .bss DATA
0004:00000000 00000014H .idata$2 DATA
0004:00000014 00000014H .idata$3 DATA
0004:00000028 00000110H .idata$4 DATA
0004:00000138 00000110H .idata$5 DATA
0004:00000248 000004afH .idata$6 DATA
Address Publics by Value Rva+Base Lib:Object
0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo.obj
0001:00000070 _main 00401070 f CrashDemo.obj
0004:00000000 __IMPORT_DESCRIPTOR_KERNEL32 00424000 kernel32:KERNEL32.dll
0004:00000014 __NULL_IMPORT_DESCRIPTOR 00424014 kernel32:KERNEL32.dll
0004:00000138 __imp__GetCommandLineA@0 00424138 kernel32:KERNEL32.dll
0004:0000013c __imp__GetVersion@0 0042413c kernel32:KERNEL32.dll
0004:00000140 __imp__ExitProcess@4 00424140 kernel32:KERNEL32.dll
0004:00000144 __imp__DebugBreak@0 00424144 kernel32:KERNEL32.dll
0004:00000148 __imp__GetStdHandle@4 00424148 kernel32:KERNEL32.dll
0004:0000014c __imp__WriteFile@20 0042414c kernel32:KERNEL32.dll
0004:00000150 __imp__InterlockedDecrement@4 00424150 kernel32:KERNEL32.dll
0004:00000154 __imp__OutputDebugStringA@4 00424154 kernel32:KERNEL32.dll
0004:00000158 __imp__GetProcAddress@8 00424158 kernel32:KERNEL32.dll
0004:0000015c __imp__LoadLibraryA@4 0042415c kernel32:KERNEL32.dll
0004:00000160 __imp__InterlockedIncrement@4 00424160 kernel32:KERNEL32.dll
0004:00000164 __imp__GetModuleFileNameA@12 00424164 kernel32:KERNEL32.dll
0004:00000168 __imp__TerminateProcess@8 00424168 kernel32:KERNEL32.dll
0004:0000016c __imp__GetCurrentProcess@0 0042416c kernel32:KERNEL32.dll
0004:00000170 __imp__UnhandledExceptionFilter@4 00424170 kernel32:KERNEL32.dll
0004:00000174 __imp__FreeEnvironmentStringsA@4 00424174 kernel32:KERNEL32.dll
0004:00000178 __imp__FreeEnvironmentStringsW@4 00424178 kernel32:KERNEL32.dll
0004:0000017c __imp__WideCharToMultiByte@32 0042417c kernel32:KERNEL32.dll
0004:00000180 __imp__GetEnvironmentStrings@0 00424180 kernel32:KERNEL32.dll
0004:00000184 __imp__GetEnvironmentStringsW@0 00424184 kernel32:KERNEL32.dll
0004:00000188 __imp__SetHandleCount@4 00424188 kernel32:KERNEL32.dll
0004:0000018c __imp__GetFileType@4 0042418c kernel32:KERNEL32.dll
0004:00000190 __imp__GetStartupInfoA@4 00424190 kernel32:KERNEL32.dll
0004:00000194 __imp__HeapDestroy@4 00424194 kernel32:KERNEL32.dll
0004:00000198 __imp__HeapCreate@12 00424198 kernel32:KERNEL32.dll
0004:0000019c __imp__HeapFree@12 0042419c kernel32:KERNEL32.dll
0004:000001a0 __imp__VirtualFree@12 004241a0 kernel32:KERNEL32.dll
0004:000001a4 __imp__RtlUnwind@16 004241a4 kernel32:KERNEL32.dll
0004:000001a8 __imp__GetLastError@0 004241a8 kernel32:KERNEL32.dll
0004:000001ac __imp__SetConsoleCtrlHandler@8 004241ac kernel32:KERNEL32.dll
0004:000001b0 __imp__IsBadWritePtr@8 004241b0 kernel32:KERNEL32.dll
0004:000001b4 __imp__IsBadReadPtr@8 004241b4 kernel32:KERNEL32.dll
0004:000001b8 __imp__HeapValidate@12 004241b8 kernel32:KERNEL32.dll
0004:000001bc __imp__GetCPInfo@8 004241bc kernel32:KERNEL32.dll
0004:000001c0 __imp__GetACP@0 004241c0 kernel32:KERNEL32.dll
0004:000001c4 __imp__GetOEMCP@0 004241c4 kernel32:KERNEL32.dll
0004:000001c8 __imp__HeapAlloc@12 004241c8 kernel32:KERNEL32.dll
0004:000001cc __imp__VirtualAlloc@16 004241cc kernel32:KERNEL32.dll
0004:000001d0 __imp__HeapReAlloc@16 004241d0 kernel32:KERNEL32.dll
0004:000001d4 __imp__MultiByteToWideChar@24 004241d4 kernel32:KERNEL32.dll
0004:000001d8 __imp__LCMapStringA@24 004241d8 kernel32:KERNEL32.dll
0004:000001dc __imp__LCMapStringW@24 004241dc kernel32:KERNEL32.dll
0004:000001e0 __imp__GetStringTypeA@20 004241e0 kernel32:KERNEL32.dll
0004:000001e4 __imp__GetStringTypeW@16 004241e4 kernel32:KERNEL32.dll
0004:000001e8 __imp__SetFilePointer@16 004241e8 kernel32:KERNEL32.dll
0004:000001ec __imp__SetStdHandle@8 004241ec kernel32:KERNEL32.dll
0004:000001f0 __imp__FlushFileBuffers@4 004241f0 kernel32:KERNEL32.dll
0004:000001f4 __imp__CloseHandle@4 004241f4 kernel32:KERNEL32.dll
0004:000001f8 \177KERNEL32_NULL_THUNK_DATA 004241f8 kernel32:KERNEL32.dll
entry point at 0001:000000f0
Line numbers for .\Debug\CrashDemo.obj(d:\msdev\myprojects\crashdemo\crashdemo.cpp) segment .text
13 0001:00000020 14 0001:00000038 15 0001:0000003f 16 0001:00000046
17 0001:00000050 20 0001:00000070 21 0001:00000088 22 0001:0000008d
如果仔細瀏覽 Rva+Base 這欄,你會發(fā)現第一個比崩潰地址 0x0040104a 大的函數地址是 0x00401070 ,所以在 0x00401070 這個地址之前的那個入口就是產生崩潰的函數,也就是這行:
0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo.obj
因此,發(fā)生崩潰的函數就是 ?Crash@@YAXXZ ,所有以問號開頭的函數名稱都是 C++ 修飾的名稱。在我們的源程序中,也就是 Crash() 這個子函數。
OK,現在我們輕而易舉地便知道了發(fā)生崩潰的函數名稱,你是不是很興奮呢?呵呵,先別忙,接下來,更厲害的招數要出場了。
請注意 MAP 文件的最后部分——代碼行信息(Line numbers information),它是以這樣的形式顯示的:
13 0001:00000020
第一個數字代表在源代碼中的代碼行號,第二個數是該代碼行在所屬的代碼段中的偏移量。
如果要查找代碼行號,需要使用下面的公式做一些十六進制的減法運算:
崩潰行偏移 = 崩潰地址(Crash Address) - 基地址(ImageBase Address) - 0x1000
為什么要這樣做呢?細心的朋友可能會留意到 Rva+Base 這欄了,我們得到的崩潰地址都是由 偏移地址(Rva)+ 基地址(Base) 得來的,所以在計算行號的時候要把基地址減去,一般情況下,基地址的值是 0x00400000 。另外,由于一般的 PE 文件的代碼段都是從 0x1000 偏移開始的,所以也必須減去 0x1000 。
好了,明白了這點,我們就可以來進行小學減法計算了:
崩潰行偏移 = 0x0040104a - 0x00400000 - 0x1000 = 0x4a
如果瀏覽 MAP 文件的代碼行信息,會看到不超過計算結果,但卻最接近的數是 CrashDemo.cpp 文件中的:
16 0001:00000046
也就是在源代碼中的第 16 行,讓我們來看看源代碼:
16 i /= j;
哈?。?!果然就是第 16 行??!
興奮嗎?我也一樣! :)
方法已經介紹完了,從今以后,我們就可以精確地定位到源代碼中的崩潰行,而且只要編譯器可以生成 MAP 文件(包括 VC、MASM、VB、BCB、Delphi……),本方法都是適用的。我們時常抱怨 M$ 的產品如何如何差,但其實 M$ 還是有意無意間提供了很多有價值的信息給我們的,只是我們往往不懂得怎么利用而已……相信這樣一來,你就可以更為從容地面對“非法操作”提示了。你甚至可以要求用戶提供崩潰的地址,然后就可以坐在家中舒舒服服地找到出錯的那行,并進行修正。
是不是很爽呢? :)
對“僅通過崩潰地址找出源代碼的出錯行”一文的補充與改進
作者:上海偉功通信 roc
下載源代碼
讀了老羅的“僅通過崩潰地址找出源代碼的出錯行”(下稱"羅文")一文后,感覺該文還是可以學到不少東西的。不過文中尚存在有些說法不妥,以及有些操作太繁瑣的地方 。為此,本人在學習了此文后,在多次實驗實踐基礎上,把該文中的一些內容進行補充與改進,希望對大家調試程序,尤其是release版本的程序有幫助 。歡迎各位朋友批評指正。
一、該方法適用的范圍
在windows程序中造成程序崩潰的原因很多,而文中所述的方法僅適用與:由一條語句當即引起的程序崩潰。如原文中舉的除數為零的崩潰例子。而筆者在實際工作中碰到更多的情況是:指針指向一非法地址 ,然后對指針的內容進行了,讀或寫的操作。例如:
void Crash1()
{
char * p =(char*)100;
*p=100;
} 這些原因造成的崩潰,無論是debug版本,還是release版本的程序,使用該方法都可找到造成崩潰的函數或子程序中的語句行,具體方法的下面還會補充說明。 另外,實踐中另一種常見的造成程序崩潰的原因:函數或子程序中局部變量數組越界付值,造成函數或子程序返回地址遭覆蓋,從而造成函數或子程序返回時崩潰。例如:
#include
void Crash2();
int main(int argc,char* argv[])
{
Crash2();
return 0;
}
void Crash2()
{
char p[1];
strcpy(p,"0123456789");
}在vc中編譯運行此程序的release版本,會跳出如下的出錯提示框。
圖一 上面例子運行結果
這里顯示的崩潰地址為:0x34333231。這種由前面語句造成的崩潰根源,在后續(xù)程序中方才顯現出來的情況,顯然用該文所述的方法就無能為力了。不過在此例中多少還有些蛛絲馬跡可尋找到崩潰的原因:函數Crash2中的局部數組p只有一個字節(jié)大小 ,顯然拷貝"0123456789"這個字符串會把超出長度的字符串拷貝到數組p的后面,即*(p+1)=''1'',*(p+2)=''2'',*(p+3)=''3'',*(p+4)=4。。。。。。而字符''1''的ASC碼的值為0x31,''2''為0x32,''3''為0x33,''4''為0x34。。。。。,由于intel的cpu中int型數據是低字節(jié)保存在低地址中 ,所以保存字符串''1234''的內存,顯示為一個4字節(jié)的int型數時就是0x34333231。顯然拷貝"0123456789"這個字符串時,"1234"這幾個字符把函數Crash2的返回地址給覆蓋 ,從而造成程序崩潰。對于類似的這種造成程序崩潰的錯誤朋友們還有其他方法排錯的話,歡迎一起交流討論。
二、設置編譯產生map文件的方法
該文中產生map文件的方法是手工添加編譯參數來產生map文件。其實在vc6的IDE中有產生map文件的配置選項的。操作如下:先點擊菜單"Project"->"Settings。。。",彈出的屬性頁中選中"Link"頁 ,確保在"category"中選中"General",最后選中"Generate mapfile"的可選項。若要在在map文件中顯示Line numbers的信息的話 ,還需在project options 中加入/mapinfo:lines 。Line numbers信息對于"羅文"所用的方法來定位出錯源代碼行很重要 ,但筆者后面會介紹更加好的方法來定位出錯代碼行,那種方法不需要Line numbers信息。
圖二 設置產生MAP文件
三、定位崩潰語句位置的方法
"羅文"所述的定位方法中,找到產生崩潰的函數位置的方法是正確的,即在map文件列出的每個函數的起始地址中,最近的且不大于崩潰地址的地址即為包含崩潰語句的函數的地址 。但之后的再進一步的定位出錯語句行的方法不是最妥當,因為那種方法前提是,假設基地址的值是 0x00400000 ,以及一般的 PE 文件的代碼段都是從 0x1000偏移開始的 。雖然這種情況很普遍,但在vc中還是可以基地址設置為其他數,比如設置為0x00500000,這時仍舊套用
崩潰行偏移 = 崩潰地址 - 0x00400000 - 0x1000 的公式顯然無法找到崩潰行偏移。 其實上述公式若改為
崩潰行偏移 = 崩潰地址 - 崩潰函數絕對地址 + 函數相對偏移即可通用了。仍以"羅文"中的例子為例:"羅文"中提到的在其崩潰程序的對應map文件中,崩潰函數的編譯結果為
0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo。obj 對與上述結果,在使用我的公式時 ,"崩潰函數絕對地址"指00401020, 函數相對偏移指 00000020, 當崩潰地址= 0x0040104a時, 則 崩潰行偏移 = 崩潰地址 - 崩潰函數起始地址+ 函數相對偏移 = 0x0040104a - 0x00401020 + 0x00000020= 0x4a,結果與"羅文"計算結果相同 。但這個公式更通用。
四、更好的定位崩潰語句位置的方法。
其實除了依靠map文件中的Line numbers信息最終定位出錯語句行外,在vc6中我們還可以通過編譯程序產生的對應的匯編語句,二進制碼,以及對應c/c++語句為一體的"cod"文件來定位出錯語句行 。先介紹一下產生這種包含了三種信息的"cod"文件的設置方法:先點擊菜單"Project"->"Settings。。。",彈出的屬性頁中選中"C/C++"頁 ,然后在"Category"中選則"Listing Files",再在"Listing file type"的組合框中選擇"Assembly,Machine code, and source"。接下去再通過一個具體的例子來說明這種方法的具體操作。
圖三 設置產生"cod"文件
準備步驟1)產生崩潰的程序如下:
01 //****************************************************************
02 //文件名稱:crash。cpp
03 //作用: 演示通過崩潰地址找出源代碼的出錯行新方法
04 //作者: 偉功通信 roc
05 //日期: 2005-5-16
06//****************************************************************
07 void Crash1();
08 int main(int argc,char* argv[])
09 {
10 Crash1();
11 return 0;
12 }
13
14 void Crash1()
15 {
16 char * p =(char*)100;
17 *p=100;
18 }
準備步驟2)按本文所述設置產生map文件(不需要產生Line numbers信息)。
準備步驟3)按本文所述設置產生cod文件。
準備步驟4)編譯。這里以debug版本為例(若是release版本需要將編譯選項改為不進行任何優(yōu)化的選項,否則上述代碼會因為優(yōu)化時看作廢代碼而不被編譯,從而看不到崩潰的結果),編譯后產生一個"exe"文件 ,一個"map"文件,一個"cod"文件。
運行此程序,產生如下如下崩潰提示:
圖四 上面例子運行結果
排錯步驟1)定位崩潰函數??梢圆樵僲ap文件獲得。我的機器編譯產生的map文件的部分如下:
Crash
Timestamp is 42881a01 (Mon May 16 11:56:49 2005)
Preferred load address is 00400000
Start Length Name Class
0001:00000000 0000ddf1H .text CODE
0001:0000ddf1 0001000fH .textbss CODE
0002:00000000 00001346H .rdata DATA
0002:00001346 00000000H .edata DATA
0003:00000000 00000104H .CRT$XCA DATA
0003:00000104 00000104H .CRT$XCZ DATA
0003:00000208 00000104H .CRT$XIA DATA
0003:0000030c 00000109H .CRT$XIC DATA
0003:00000418 00000104H .CRT$XIZ DATA
0003:0000051c 00000104H .CRT$XPA DATA
0003:00000620 00000104H .CRT$XPX DATA
0003:00000724 00000104H .CRT$XPZ DATA
0003:00000828 00000104H .CRT$XTA DATA
0003:0000092c 00000104H .CRT$XTZ DATA
0003:00000a30 00000b93H .data DATA
0003:000015c4 00001974H .bss DATA
0004:00000000 00000014H .idata$2 DATA
0004:00000014 00000014H .idata$3 DATA
0004:00000028 00000110H .idata$4 DATA
0004:00000138 00000110H .idata$5 DATA
0004:00000248 000004afH .idata$6 DATA
Address Publics by Value Rva+Base Lib:Object
0001:00000020 _main 00401020 f Crash.obj
0001:00000060 ?Crash1@@YAXXZ 00401060 f Crash.obj
0001:000000a0 __chkesp 004010a0 f LIBCD:chkesp.obj
0001:000000e0 _mainCRTStartup 004010e0 f LIBCD:crt0.obj
0001:00000210 __amsg_exit 00401210 f LIBCD:crt0.obj
0001:00000270 __CrtDbgBreak 00401270 f LIBCD:dbgrpt.obj
...
對于崩潰地址0x00401082而言,小于此地址中最接近的地址(Rva+Base中的地址)為00401060,其對應的函數名為?Crash1@@YAXXZ,由于所有以問號開頭的函數名稱都是 C++ 修飾的名稱 ,"@@YAXXZ"則為區(qū)別重載函數而加的后綴,所以?Crash1@@YAXXZ就是我們的源程序中,Crash1() 這個函數。
排錯步驟2)定位出錯行。打開編譯生成的"cod"文件,我機器上生成的文件內容如下:
TITLE E:\Crash\Crash。cpp
.386P
include listing.inc
if @Version gt 510
.model FLAT
else
_TEXT SEGMENT PARA USE32 PUBLIC ''CODE''
_TEXT ENDS
_DATA SEGMENT DWORD USE32 PUBLIC ''DATA''
_DATA ENDS
CONST SEGMENT DWORD USE32 PUBLIC ''CONST''
CONST ENDS
_BSS SEGMENT DWORD USE32 PUBLIC ''BSS''
_BSS ENDS
$SYMBOLS SEGMENT BYTE USE32 ''DEBSYM''
$SYMBOLS ENDS
$TYPES SEGMENT BYTE USE32 ''DEBTYP''
$TYPES ENDS
_TLS SEGMENT DWORD USE32 PUBLIC ''TLS''
_TLS ENDS
; COMDAT _main
_TEXT SEGMENT PARA USE32 PUBLIC ''CODE''
_TEXT ENDS
; COMDAT ?Crash1@@YAXXZ
_TEXT SEGMENT PARA USE32 PUBLIC ''CODE''
_TEXT ENDS
FLAT GROUP _DATA, CONST, _BSS
ASSUME CS: FLAT, DS: FLAT, SS: FLAT
endif
PUBLIC ?Crash1@@YAXXZ ; Crash1
PUBLIC _main
EXTRN __chkesp:NEAR
; COMDAT _main
_TEXT SEGMENT
_main PROC NEAR ; COMDAT
; 9 : {
00000 55 push ebp
00001 8b ec mov ebp, esp
00003 83 ec 40 sub esp, 64 ; 00000040H
00006 53 push ebx
00007 56 push esi
00008 57 push edi
00009 8d 7d c0 lea edi, DWORD PTR [ebp-64]
0000c b9 10 00 00 00 mov ecx, 16 ; 00000010H
00011 b8 cc cc cc cc mov eax, -858993460 ; ccccccccH
00016 f3 ab rep stosd
; 10 : Crash1();
00018 e8 00 00 00 00 call ?Crash1@@YAXXZ ; Crash1
; 11 : return 0;
0001d 33 c0 xor eax, eax
; 12 : }
0001f 5f pop edi
00020 5e pop esi
00021 5b pop ebx
00022 83 c4 40 add esp, 64 ; 00000040H
00025 3b ec cmp ebp, esp
00027 e8 00 00 00 00 call __chkesp
0002c 8b e5 mov esp, ebp
0002e 5d pop ebp
0002f c3 ret 0
_main ENDP
_TEXT ENDS
; COMDAT ?Crash1@@YAXXZ
_TEXT SEGMENT
_p$ = -4
?Crash1@@YAXXZ PROC NEAR ; Crash1, COMDAT
; 15 : {
00000 55 push ebp
00001 8b ec mov ebp, esp
00003 83 ec 44 sub esp, 68 ; 00000044H
00006 53 push ebx
00007 56 push esi
00008 57 push edi
00009 8d 7d bc lea edi, DWORD PTR [ebp-68]
0000c b9 11 00 00 00 mov ecx, 17 ; 00000011H
00011 b8 cc cc cc cc mov eax, -858993460 ; ccccccccH
00016 f3 ab rep stosd
; 16 : char * p =(char*)100;
00018 c7 45 fc 64 00
00 00 mov DWORD PTR _p$[ebp], 100 ; 00000064H
; 17 : *p=100;
0001f 8b 45 fc mov eax, DWORD PTR _p$[ebp]
00022 c6 00 64 mov BYTE PTR [eax], 100 ; 00000064H
; 18 : }
00025 5f pop edi
00026 5e pop esi
00027 5b pop ebx
00028 8b e5 mov esp, ebp
0002a 5d pop ebp
0002b c3 ret 0
?Crash1@@YAXXZ ENDP ; Crash1
_TEXT ENDS
END
其中
?Crash1@@YAXXZ PROC NEAR ; Crash1, COMDAT為Crash1匯編代碼的起始行。產生崩潰的代碼便在其后的某個位置。接下去的一行為:
; 15 : {冒號后的"{"表示源文件中的語句,冒號前的"15"表示該語句在源文件中的行數。 這之后顯示該語句匯編后的偏移地址,二進制碼,匯編代碼。如
00000 55 push ebp其中"0000"表示相對于函數開始地址后的偏移,"55"為編譯后的機器代碼," push ebp"為匯編代碼。從"cod"文件中我們可以看出,一條(c/c++)語句通常需要編譯成數條匯編語句 。此外有些匯編語句太長則會分兩行顯示如:
00018 c7 45 fc 64 00
00 00 mov DWORD PTR _p$[ebp], 100 ; 00000064H其中"0018"表示相對偏移,在debug版本中,這個數據為相對于函數起始地址的偏移(此時每個函數第一條語句相對偏移為0000);release版本中為相對于代碼段第一條語句的偏移(即代碼段第一條語句相對偏移為0000,而以后的每個函數第一條語句相對偏移就不為0000了)。"c7 45 fc 64 00 00 00 "為編譯后的機器代碼 ,"mov DWORD PTR _p$[ebp], 100"為匯編代碼, 匯編語言中";"后的內容為注釋,所以";00000064H",是個注釋這里用來說明100轉換成16進制時為"00000064H"。
接下去,我們開始來定位產生崩潰的語句。
第一步,計算崩潰地址相對于崩潰函數的偏移,在本例中已經知道了崩潰語句的地址(0x00401082),和對應函數的起始地址(0x00401060),所以崩潰地址相對函數起始地址的偏移就很容易計算了:
崩潰偏移地址 = 崩潰語句地址 - 崩潰函數的起始地址 = 0x00401082 - 0x00401060 = 0x22。第二步,計算出錯的匯編語句在cod文件中的相對偏移。我們可以看到函數Crash1()在cod文件中的相對偏移地址為0000,則
崩潰語句在cod文件中的相對偏移 = 崩潰函數在cod文件中相對偏移 + 崩潰偏移地址 = 0x0000 + 0x22 = 0x22第三步,我們看Crash1函數偏移0x22除的代碼是什么?結果如下
00022 c6 00 64 mov BYTE PTR [eax], 100 ; 00000064H這句匯編語句表示將100這個數保存到寄存器eax所指的內存單元中去,保存空間大小為1個字節(jié)(byte)。程序正是執(zhí)行這條命令時產生了崩潰,顯然這里eax中的為一個非法地址 ,所以程序崩潰了!
第四步,再查看該匯編語句在其前面幾行的其對應的源代碼,結果如下:
; 17 : *p=100;其中17表示該語句位于源文件中第17行,而“*p=100;”這正是源文件中產生崩潰的語句。
至此我們僅從崩潰地址就查找出了造成崩潰的源代碼語句和該語句所在源文件中的確切位置,甚至查找到了造成崩潰的編譯后的確切匯編代碼!
怎么樣,是不是感覺更爽啊?
五、小節(jié)
1、新方法同樣要注意可以適用的范圍,即程序由一條語句當即引起的崩潰。另外我不知道除了VC6外,是否還有其他的編譯器能夠產生類似的"cod"文件。
2、我們可以通過比較 新方法產生的debug和releae版本的"cod"文件,查找那些僅release版本(或debug版本)有另一個版本沒有的bug(或其他性狀)。例如"羅文"中所舉的那個用例 ,只要打開release版本的"cod"文件,就明白了為啥debug版本會產生崩潰而release版本卻沒有:原來release版本中產生崩潰的語句其實根本都沒有編譯 。同樣本例中的release版本要看到崩潰的效果,需要將編譯選項改為為不優(yōu)化的配置。
關于MFC下檢查和消除內存泄露的技巧
作者:freepublic
摘要
本文分析了Windows環(huán)境使用MFC調試內存泄露的技術,介紹了在Windows環(huán)境下用VC++查找,定位和消除內存泄露的方法技巧。
關鍵詞:VC++;CRT 調試堆函數;試探法。
編譯環(huán)境
VC++6.0
技術原理
檢測內存泄漏的主要工具是調試器和 CRT 調試堆函數。若要啟用調試堆函數,請在程序中包括以下語句:
#define CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>注意 #include 語句必須采用上文所示順序。如果更改了順序,所使用的函數可能無法正確工作。
通過包括 crtdbg.h,將 malloc 和 free 函數映射到其“Debug”版本_malloc_dbg 和_free_dbg,這些函數將跟蹤內存分配和釋放。此映射只在調試版本(在其中定義了 _DEBUG)中發(fā)生。發(fā)布版本使用普通的 malloc 和 free 函數。
#define 語句將 CRT 堆函數的基版本映射到對應的“Debug”版本。并非絕對需要該語句,但如果沒有該語句,內存泄漏轉儲包含的有用信息將較少。
在添加了上面所示語句之后,可以通過在程序中包括以下語句來轉儲內存泄漏信息:
_CrtDumpMemoryLeaks();當在調試器下運行程序時,_CrtDumpMemoryLeaks 將在“輸出”窗口中顯示內存泄漏信息。內存泄漏信息如下所示:
Detected memory leaks!
Dumping objects ->
C:PROGRAM FILESVISUAL STUDIOMyProjectsleaktestleaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete. 如果不使用 #define _CRTDBG_MAP_ALLOC 語句,內存泄漏轉儲如下所示:
Detected memory leaks!
Dumping objects ->
{18} normal block at 0x00780E80, 64 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete. 未定義 _CRTDBG_MAP_ALLOC 時,所顯示的會是:
內存分配編號(在大括號內)。
塊類型(普通、客戶端或 CRT)。
十六進制形式的內存位置。
以字節(jié)為單位的塊大小。
前 16 字節(jié)的內容(亦為十六進制)。
定義了 _CRTDBG_MAP_ALLOC 時,還會顯示在其中分配泄漏的內存的文件。文件名后括號中的數字(本示例中為 20)是該文件內的行號。
轉到源文件中分配內存的行
在"輸出"窗口中雙擊包含文件名和行號的行。
-或-
在"輸出"窗口中選擇包含文件名和行號的行,然后按 F4 鍵。
_CrtSetDbgFlag 如果程序總在同一位置退出,則調用 _CrtDumpMemoryLeaks 足夠方便,但如果程序可以從多個位置退出該怎么辦呢?不要在每個可能的出口放置一個對 _CrtDumpMemoryLeaks 的調用,可以在程序開始包括以下調用:
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); 該語句在程序退出時自動調用 _CrtDumpMemoryLeaks。必須同時設置 _CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF 兩個位域,如上所示。
說明
在VC++6.0的環(huán)境下,不再需要額外的添加
#define CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h> 只需要按F5,在調試狀態(tài)下運行,程序退出后在"輸出窗口"可以看到有無內存泄露。如果出現
Detected memory leaks!
Dumping objects -> 就有內存泄露。
確定內存泄露的地方
根據內存泄露的報告,有兩種消除的方法:
第一種比較簡單,就是已經把內存泄露映射到源文件的,可以直接在"輸出"窗口中雙擊包含文件名和行號的行。例如
Detected memory leaks!
Dumping objects ->
C:PROGRAM FILESVISUAL STUDIOMyProjectsleaktestleaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
C:PROGRAM FILESVISUAL STUDIOMyProjectsleaktestleaktest.cpp(20)就是源文件名稱和行號。
第二種比較麻煩,就是不能映射到源文件的,只有內存分配塊號。
Detected memory leaks!
Dumping objects ->
{18} normal block at 0x00780E80, 64 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete. 這種情況我采用一種"試探法"。由于內存分配的塊號不是固定不變的,而是每次運行都是變化的,所以跟蹤起來很麻煩。但是我發(fā)現雖然內存分配的塊號是變化的,但是變化的塊號卻總是那幾個,也就是說多運行幾次,內存分配的塊號很可能會重復。因此這就是"試探法"的基礎。
先在調試狀態(tài)下運行幾次程序,觀察內存分配的塊號是哪幾個值;
選擇出現次數最多的塊號來設斷點,在代碼中設置內存分配斷點: 添加如下一行(對于第 18 個內存分配):
_crtBreakAlloc = 18; 或者,可以使用具有同樣效果的 _CrtSetBreakAlloc 函數:
_CrtSetBreakAlloc(18);
在調試狀態(tài)下運行序,在斷點停下時,打開"調用堆棧"窗口,找到對應的源代碼處;
退出程序,觀察"輸出窗口"的內存泄露報告,看實際內存分配的塊號是不是和預設值相同,如果相同,就找到了;如果不同,就重復步驟3,直到相同。
最后就是根據具體情況,在適當的位置釋放所分配的內存。
發(fā)表于 @ 2007年05月17日 15:56:00 | 評論( 0 ) | 編輯| 舉報| 收藏
舊一篇:VC++6.0中內存泄漏檢測(轉) | 新一篇:_CrtDumpMemoryLeaks()的作用(轉)
查看最新精華文章 請訪問博客首頁相關文章
vc++內存泄漏檢測使用 CRT 調試功能來檢測內存泄漏VC使用CRT調試功能來檢測內存泄漏檢測內存泄漏使用CRT調試功能來檢測內存泄漏VC使用CRT調試功能來檢測內存泄漏發(fā)表評論 表 情: 評論內容: 用 戶 名: 登錄 注冊 匿名評論 匿名用戶驗 證 碼: 重新獲得驗證碼 熱門招聘職位【《七雄爭霸》研發(fā)商:北京游戲谷】誠聘JAVA、C++技術專家及各類游戲精英【華北計算技術研究所】急聘Java開發(fā)、系統(tǒng)架構、需求分析【無錫富贏科技有限公司】誠聘 程序員ASP.NET及軟件開發(fā)工程師C++【勵展博覽集團】急聘IT Application Developer企業(yè)應用程序開發(fā)員【西安瑞祺科技】誠聘JAVA軟件工程師【雅邦網絡】誠聘網站項目經理、JAVA技術總監(jiān)、JAVA開發(fā)、網站測試工程師【科銳】誠招IT中高級人才,搶工作機會,贏IPAD啦!【imo】-國際風投+福利租房+獎金+期權+不加班+調休,邀你共創(chuàng)互聯網的奇跡【YOHO!新力傳媒】高薪誠聘各類網站人才 北京+南京【方正國際】誠招軟件精英 北京+蘇州+武漢【熱聘】搜狐暢游全國熱招開發(fā)工程師【愛立信上?!考闭卸嗝襟w、核心網開發(fā)測試工程師,國際團隊等你加盟! 【《七雄爭霸》研發(fā)商:北京游戲谷】誠聘JAVA、C++技術專家及各類游戲精英【華北計算技術研究所】急聘Java開發(fā)、系統(tǒng)架構、需求分析【無錫富贏科技有限公司】誠聘 程序員ASP.NET及軟件開發(fā)工程師C++【勵展博覽集團】急聘IT Application Developer企業(yè)應用程序開發(fā)員【西安瑞祺科技】誠聘JAVA軟件工程師【雅邦網絡】誠聘網站項目經理、JAVA技術總監(jiān)、JAVA開發(fā)、網站測試工程師【科銳】誠招IT中高級人才,搶工作機會,贏IPAD啦!【imo】-國際風投+福利租房+獎金+期權+不加班+調休,邀你共創(chuàng)互聯網的奇跡【YOHO!新力傳媒】高薪誠聘各類網站人才 北京+南京【方正國際】誠招軟件精英 北京+蘇州+武漢【熱聘】搜狐暢游全國熱招開發(fā)工程師【愛立信上?!考闭卸嗝襟w、核心網開發(fā)測試工程師,國際團隊等你加盟! 公司簡介|招賢納士|廣告服務|銀行匯款賬號|聯系方式|版權聲明|法律顧問|問題報告
北京創(chuàng)新樂知信息技術有限公司 版權所有, 京 ICP 證 070598 號
世紀樂知(北京)網絡技術有限公司 提供技術支持
江蘇樂知網絡技術有限公司 提供商務支持
Email:webmaster@csdn.net
Copyright © 1999-2010, CSDN.NET, All Rights Reserved
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/bairny/archive/2007/05/17/1613431.aspx