您還未登錄!|登錄|注冊(cè)|幫助
CSDN首頁(yè) 資訊 論壇 博客 下載 搜索 更多CTO俱樂(lè)部
學(xué)生大本營(yíng)
培訓(xùn)充電
移動(dòng)開發(fā)
軟件研發(fā)
云計(jì)算
程序員
TUP
bairny的專欄
條新通知 登錄 注冊(cè) 歡迎 退出 我的博客 配置 寫文章 文章管理 博客首頁(yè) 全站 當(dāng)前博客 空間 博客 好友 相冊(cè) 留言 用戶操作
[留言] [發(fā)消息] [加為好友]
byID:bairny
共16105次訪問(wèn),排名11515,好友0人,關(guān)注者1人。
by的文章
原創(chuàng) 10 篇
翻譯 0 篇
轉(zhuǎn)載 11 篇
評(píng)論 5 篇
訂閱我的博客
[編輯]bairny的公告
[編輯]文章分類
PE文件格式
WTL
存檔
2007年08月(4)
2007年07月(3)
2007年05月(11)
2007年03月(3)
公告: 第三屆中國(guó)云計(jì)算大會(huì) 六折票價(jià)搶購(gòu)中! [意見反饋][官方博客] VC++ 6.0 中如何使用 CRT 調(diào)試功能來(lái)檢測(cè)內(nèi)存泄漏(轉(zhuǎn)) 收藏
VC++ 6.0 中如何使用 CRT 調(diào)試功能來(lái)檢測(cè)內(nèi)存泄漏
作者:JerryZ
下載例子源代碼
最近看了周星星 Blog 中的一篇文章:“VC++6.0中內(nèi)存泄漏檢測(cè)”,受益匪淺,便運(yùn)行其例子代碼想看看 Output 窗口中的輸出結(jié)果,可惜怎么弄其輸出都不是預(yù)期的東西,郁悶了半天,便到水壇里找到周星星,請(qǐng)求他指點(diǎn)一、二,然而未果。沒(méi)有辦法,最后我一頭栽進(jìn) MSDN 庫(kù)狂搜了一把,功夫不負(fù)有心人,我搜出很多有關(guān)這方面的資料,沒(méi)過(guò)多久我便基本上就找到了答案......
首先,檢測(cè)內(nèi)存泄漏的基本工具是調(diào)試器和 CRT 調(diào)試堆函數(shù)。為了使用調(diào)試堆函數(shù),必須在要檢測(cè)內(nèi)存泄漏和調(diào)試的程序中添加下面的語(yǔ)句:
#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h>
#include "debug_new.h" MSDN 如是說(shuō):“必須保證上面聲明的順序,如果改變了順序,可能不能正常工作。”至于這是為什么,我們不得而知。MS 的老大們經(jīng)常這樣故弄玄虛。
針對(duì)非 MFC 程序,再加上周星星的頭文件:debug_new.h,當(dāng)然如果不加這一句,也能檢測(cè)出內(nèi)存泄漏,但是你無(wú)法確定在哪個(gè)源程序文件中發(fā)生泄漏。Output 輸出只告訴你在 crtsdb.h 中的某個(gè)地方有內(nèi)存泄漏。我測(cè)試時(shí) REG_DEBUG_NEW 沒(méi)有起作用。加不加這個(gè)宏都可以檢測(cè)出發(fā)生內(nèi)存分配泄漏的文件。
其次,一旦添加了上面的聲明,你就可以通過(guò)在程序中加入下面的代碼來(lái)報(bào)告內(nèi)存泄漏信息了:
_CrtDumpMemoryLeaks(); 這就這么簡(jiǎn)單。我在周星星的例子代碼中加入這些機(jī)關(guān)后,在 VC++ 調(diào)試會(huì)話(按 F5 調(diào)試運(yùn)行) Output 窗口的 Debug 頁(yè)便看到了預(yù)期的內(nèi)存泄漏 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. 更具體的細(xì)節(jié)請(qǐng)參考本文附帶的源代碼文件。
下面是我看過(guò) MSDN 資料后,針對(duì)“如何使用 CRT 調(diào)試功能來(lái)檢測(cè)內(nèi)存泄漏?”的問(wèn)題進(jìn)行了一番編譯和整理,希望對(duì)大家有用。如果你的英文很棒,那就不用往下看了,建議直接去讀 MSDN 庫(kù)中的技術(shù)原文。
C/C++ 編程語(yǔ)言的最強(qiáng)大功能之一便是其動(dòng)態(tài)分配和釋放內(nèi)存,但是中國(guó)有句古話:“最大的長(zhǎng)處也可能成為最大的弱點(diǎn)”,那么 C/C++ 應(yīng)用程序正好印證了這句話。在 C/C++ 應(yīng)用程序開發(fā)過(guò)程中,動(dòng)態(tài)分配的內(nèi)存處理不當(dāng)是最常見的問(wèn)題。其中,最難捉摸也最難檢測(cè)的錯(cuò)誤之一就是內(nèi)存泄漏,即未能正確釋放以前分配的內(nèi)存的錯(cuò)誤。偶爾發(fā)生的少量?jī)?nèi)存泄漏可能不會(huì)引起我們的注意,但泄漏大量?jī)?nèi)存的程序或泄漏日益增多的程序可能會(huì)表現(xiàn)出各種 各樣的征兆:從性能不良(并且逐漸降低)到內(nèi)存完全耗盡。更糟的是,泄漏的程序可能會(huì)用掉太多內(nèi)存,導(dǎo)致另外一個(gè)程序垮掉,而使用戶無(wú)從查找問(wèn)題的真正根源。此外,即使無(wú)害的內(nèi)存泄漏也可能殃及池魚。
幸運(yùn)的是,Visual Studio 調(diào)試器和 C 運(yùn)行時(shí) (CRT) 庫(kù)為我們提供了檢測(cè)和識(shí)別內(nèi)存泄漏的有效方法。下面請(qǐng)和我一起分享收獲——如何使用 CRT 調(diào)試功能來(lái)檢測(cè)內(nèi)存泄漏?
如何啟用內(nèi)存泄漏檢測(cè)機(jī)制?
使用 _CrtSetDbgFlag
設(shè)置 CRT 報(bào)告模式
解釋內(nèi)存塊類型
如何在內(nèi)存分配序號(hào)處設(shè)置斷點(diǎn)?
如何比較內(nèi)存狀態(tài)?
結(jié)論
如何啟用內(nèi)存泄漏檢測(cè)機(jī)制?
VC++ IDE 的默認(rèn)狀態(tài)是沒(méi)有啟用內(nèi)存泄漏檢測(cè)機(jī)制的,也就是說(shuō)即使某段代碼有內(nèi)存泄漏,調(diào)試會(huì)話的 Output 窗口的 Debug 頁(yè)不會(huì)輸出有關(guān)內(nèi)存泄漏信息。你必須設(shè)定兩個(gè)最基本的機(jī)關(guān)來(lái)啟用內(nèi)存泄漏檢測(cè)機(jī)制。
一是使用調(diào)試堆函數(shù):
#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h> 注意:#include 語(yǔ)句的順序。如果更改此順序,所使用的函數(shù)可能無(wú)法正確工作。
通過(guò)包含 crtdbg.h 頭文件,可以將 malloc 和 free 函數(shù)映射到其“調(diào)試”版本 _malloc_dbg 和 _free_dbg,這些函數(shù)會(huì)跟蹤內(nèi)存分配和釋放。此映射只在調(diào)試(Debug)版本(也就是要定義 _DEBUG)中有效。發(fā)行版本(Release)使用普通的 malloc 和 free 函數(shù)。
#define 語(yǔ)句將 CRT 堆函數(shù)的基礎(chǔ)版本映射到對(duì)應(yīng)的“調(diào)試”版本。該語(yǔ)句不是必須的,但如果沒(méi)有該語(yǔ)句,那么有關(guān)內(nèi)存泄漏的信息會(huì)不全。
二是在需要檢測(cè)內(nèi)存泄漏的地方添加下面這條語(yǔ)句來(lái)輸出內(nèi)存泄漏信息:
_CrtDumpMemoryLeaks(); 當(dāng)在調(diào)試器下運(yùn)行程序時(shí),_CrtDumpMemoryLeaks 將在 Output 窗口的 Debug 頁(yè)中顯示內(nèi)存泄漏信息。比如:
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 語(yǔ)句,內(nèi)存泄漏的輸出是這樣的:
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. 根據(jù)這段輸出信息,你無(wú)法知道在哪個(gè)源程序文件里發(fā)生了內(nèi)存泄漏。下面我們來(lái)研究一下輸出信息的格式。第一行和第二行沒(méi)有什么可說(shuō)的,從第三行開始:
xx}:花括弧內(nèi)的數(shù)字是內(nèi)存分配序號(hào),本文例子中是 {45},{44},{43};
block:內(nèi)存塊的類型,常用的有三種:normal(普通)、client(客戶端)或 CRT(運(yùn)行時(shí));本文例子中是:normal block;
用十六進(jìn)制格式表示的內(nèi)存位置,如:at 0x00441BA0 等;
以字節(jié)為單位表示的內(nèi)存塊的大小,如:32 bytes long;
前 16 字節(jié)的內(nèi)容(也是用十六進(jìn)制格式表示),如:Data: <AB> 41 42 等; 仔細(xì)觀察不難發(fā)現(xiàn),如果定義了 _CRTDBG_MAP_ALLOC ,那么在內(nèi)存分配序號(hào)前面還會(huì)顯示在其中分配泄漏內(nèi)存的文件名,以及文件名后括號(hào)中的數(shù)字表示發(fā)生泄漏的代碼行號(hào),比如:
C:\Temp\memleak\memleak.cpp(15) 雙擊 Output 窗口中此文件名所在的輸出行,便可跳到源程序文件分配該內(nèi)存的代碼行(也可以選中該行,然后按 F4,效果一樣) ,這樣一來(lái)我們就很容易定位內(nèi)存泄漏是在哪里發(fā)生的了,因此,_CRTDBG_MAP_ALLOC 的作用顯而易見。
使用 _CrtSetDbgFlag
如果程序只有一個(gè)出口,那么調(diào)用 _CrtDumpMemoryLeaks 的位置是很容易選擇的。但是,如果程序可能會(huì)在多個(gè)地方退出該怎么辦呢?在每一個(gè)可能的出口處調(diào)用 _CrtDumpMemoryLeaks 肯定是不可取的,那么這時(shí)可以在程序開始處包含下面的調(diào)用:
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); 這條語(yǔ)句無(wú)論程序在什么地方退出都會(huì)自動(dòng)調(diào)用 _CrtDumpMemoryLeaks。注意:這里必須同時(shí)設(shè)置兩個(gè)位域標(biāo)志:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。
設(shè)置 CRT 報(bào)告模式
默認(rèn)情況下,_CrtDumpMemoryLeaks 將內(nèi)存泄漏信息 dump 到 Output 窗口的 Debug 頁(yè), 如果你想將這個(gè)輸出定向到別的地方,可以使用 _CrtSetReportMode 進(jìn)行重置。如果你使用某個(gè)庫(kù),它可能將輸出定向到另一位置。此時(shí),只要使用以下語(yǔ)句將輸出位置設(shè)回 Output 窗口即可:
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );有關(guān)使用 _CrtSetReportMode 的詳細(xì)信息,請(qǐng)參考 MSDN 庫(kù)關(guān)于 _CrtSetReportMode 的描述。
解釋內(nèi)存塊類型
前面已經(jīng)說(shuō)過(guò),內(nèi)存泄漏報(bào)告中把每一塊泄漏的內(nèi)存分為 normal(普通塊)、client(客戶端塊)和 CRT 塊。事實(shí)上,需要留心和注意的也就是 normal 和 client,即普通塊和客戶端塊。
normal block(普通塊):這是由你的程序分配的內(nèi)存。
client block(客戶塊):這是一種特殊類型的內(nèi)存塊,專門用于 MFC 程序中需要析構(gòu)函數(shù)的對(duì)象。MFC new 操作符視具體情況既可以為所創(chuàng)建的對(duì)象建立普通塊,也可以為之建立客戶塊。
CRT block(CRT 塊):是由 C RunTime Library 供自己使用而分配的內(nèi)存塊。由 CRT 庫(kù)自己來(lái)管理這些內(nèi)存的分配與釋放,我們一般不會(huì)在內(nèi)存泄漏報(bào)告中發(fā)現(xiàn) CRT 內(nèi)存泄漏,除非程序發(fā)生了嚴(yán)重的錯(cuò)誤(例如 CRT 庫(kù)崩潰)。
除了上述的類型外,還有下面這兩種類型的內(nèi)存塊,它們不會(huì)出現(xiàn)在內(nèi)存泄漏報(bào)告中:
free block(空閑塊):已經(jīng)被釋放(free)的內(nèi)存塊。
Ignore block(忽略塊):這是程序員顯式聲明過(guò)不要在內(nèi)存泄漏報(bào)告中出現(xiàn)的內(nèi)存塊。
如何在內(nèi)存分配序號(hào)處設(shè)置斷點(diǎn)?
在內(nèi)存泄漏報(bào)告中,的文件名和行號(hào)可告訴分配泄漏的內(nèi)存的代碼位置,但僅僅依賴這些信息來(lái)了解完整的泄漏原因是不夠的。因?yàn)橐粋€(gè)程序在運(yùn)行時(shí),一段分配內(nèi)存的代碼可能會(huì)被調(diào)用很多次,只要有一次調(diào)用后沒(méi)有釋放內(nèi)存就會(huì)導(dǎo)致內(nèi)存泄漏。為了確定是哪些內(nèi)存沒(méi)有被釋放,不僅要知道泄漏的內(nèi)存是在哪里分配的,還要知道泄漏產(chǎn)生的條件。這時(shí)內(nèi)存分配序號(hào)就顯得特別有用——這個(gè)序號(hào)就是文件名和行號(hào)之后的花括弧里的那個(gè)數(shù)字。
例如,在本文例子代碼的輸出信息中,“45”是內(nèi)存分配序號(hào),意思是泄漏的內(nèi)存是你程序中分配的第四十五個(gè)內(nèi)存塊:
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 庫(kù)對(duì)程序運(yùn)行期間分配的所有內(nèi)存塊進(jìn)行計(jì)數(shù),包括由 CRT 庫(kù)自己分配的內(nèi)存和其它庫(kù)(如 MFC)分配的內(nèi)存。因此,分配序號(hào)為 N 的對(duì)象即為程序中分配的第 N 個(gè)對(duì)象,但不一定是代碼分配的第 N 個(gè)對(duì)象。(大多數(shù)情況下并非如此。)
這樣的話,你便可以利用分配序號(hào)在分配內(nèi)存的位置設(shè)置一個(gè)斷點(diǎn)。方法是在程序起始附近設(shè)置一個(gè)位置斷點(diǎn)。當(dāng)程序在該點(diǎn)中斷時(shí),可以從 QuickWatch(快速監(jiān)視)對(duì)話框或 Watch(監(jiān)視)窗口設(shè)置一個(gè)內(nèi)存分配斷點(diǎn):
例如,在 Watch 窗口中,在 Name 欄鍵入下面的表達(dá)式:
_crtBreakAlloc如果要使用 CRT 庫(kù)的多線程 DLL 版本(/MD 選項(xiàng)),那么必須包含上下文操作符,像這樣:
{,,msvcrtd.dll}_crtBreakAlloc 現(xiàn)在按下回車鍵,調(diào)試器將計(jì)算該值并把結(jié)果放入 Value 欄。如果沒(méi)有在內(nèi)存分配點(diǎn)設(shè)置任何斷點(diǎn),該值將為 –1。
用你想要在其位置中斷的內(nèi)存分配的分配序號(hào)替換 Value 欄中的值。例如輸入 45。這樣就會(huì)在分配序號(hào)為 45 的地方中斷。
在所感興趣的內(nèi)存分配處設(shè)置斷點(diǎn)后,可以繼續(xù)調(diào)試。這時(shí),運(yùn)行程序時(shí)一定要小心,要保證內(nèi)存塊分配的順序不會(huì)改變。當(dāng)程序在指定的內(nèi)存分配處中斷時(shí),可以查看 Call Stack(調(diào)用堆棧)窗口和其它調(diào)試器信息以確定分配內(nèi)存時(shí)的情況。如果必要,可以從該點(diǎn)繼續(xù)執(zhí)行程序,以查看對(duì)象發(fā)生了什么情況,或許可以確定未正確釋放對(duì)象的原因。
盡管通常在調(diào)試器中設(shè)置內(nèi)存分配斷點(diǎn)更方便,但如果愿意,也可在代碼中設(shè)置這些斷點(diǎn)。為了在代碼中設(shè)置一個(gè)內(nèi)存分配斷點(diǎn),可以增加這樣一行(對(duì)于第四十五個(gè)內(nèi)存分配):
_crtBreakAlloc = 45;你還可以使用有相同效果的 _CrtSetBreakAlloc 函數(shù):
_CrtSetBreakAlloc(45);如何比較內(nèi)存狀態(tài)?
定位內(nèi)存泄漏的另一個(gè)方法就是在關(guān)鍵點(diǎn)獲取應(yīng)用程序內(nèi)存狀態(tài)的快照。CRT 庫(kù)提供了一個(gè)結(jié)構(gòu)類型 _CrtMemState。你可以用它來(lái)存儲(chǔ)內(nèi)存狀態(tài)的快照:
_CrtMemState s1, s2, s3; 若要獲取給定點(diǎn)的內(nèi)存狀態(tài)快照,可以向 _CrtMemCheckpoint 函數(shù)傳遞一個(gè) _CrtMemState 結(jié)構(gòu)。該函數(shù)用當(dāng)前內(nèi)存狀態(tài)的快照填充此結(jié)構(gòu):
_CrtMemCheckpoint( &s1 ); 通過(guò)向 _CrtMemDumpStatistics 函數(shù)傳遞 _CrtMemState 結(jié)構(gòu),可以在任意地方 dump 該結(jié)構(gòu)的內(nèi)容:
_CrtMemDumpStatistics( &s1 );該函數(shù)輸出如下格式的 dump 內(nèi)存分配信息:
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ā)生了內(nèi)存泄漏,可以通過(guò)獲取該段代碼之前和之后的內(nèi)存狀態(tài)快照,然后使用 _CrtMemDifference 比較這兩個(gè)狀態(tài):
_CrtMemCheckpoint( &s1 );// 獲取第一個(gè)內(nèi)存狀態(tài)快照
// 在這里進(jìn)行內(nèi)存分配
_CrtMemCheckpoint( &s2 );// 獲取第二個(gè)內(nèi)存狀態(tài)快照
// 比較兩個(gè)內(nèi)存快照的差異
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );// dump 差異結(jié)果 顧名思義,_CrtMemDifference 比較兩個(gè)內(nèi)存狀態(tài)(前兩個(gè)參數(shù)),生成這兩個(gè)狀態(tài)之間差異的結(jié)果(第三個(gè)參數(shù))。在程序的開始和結(jié)尾放置 _CrtMemCheckpoint 調(diào)用,并使用 _CrtMemDifference 比較結(jié)果,是檢查內(nèi)存泄漏的另一種方法。如果檢測(cè)到泄漏,則可以使用 _CrtMemCheckpoint 調(diào)用通過(guò)二進(jìn)制搜索技術(shù)來(lái)分割程序和定位泄漏。
結(jié)論
盡管 VC ++ 具有一套專門調(diào)試 MFC 應(yīng)用程序的機(jī)制,但本文上述討論的內(nèi)存分配很簡(jiǎn)單,沒(méi)有涉及到 MFC 對(duì)象,所以這些內(nèi)容同樣也適用于 MFC 程序。在 MSDN 庫(kù)中可以找到很多有關(guān) VC++ 調(diào)試方面的資料,如果你能善用 MSDN 庫(kù),相信用不了多少時(shí)間你就有可能成為調(diào)試高手。
本人水平不高,謬誤在所難免,請(qǐng)大家拍磚,不要客氣。順祝大家圣誕快樂(lè)!
JerryZ 于 2004 年平安夜,
調(diào)試方法和技巧
作者:非凡
便于調(diào)試的代碼風(fēng)格:
不用全局變量
所有變量都要初始化,成員變量在構(gòu)造函數(shù)中初始化
盡量使用const
詳盡的注釋
VC++編譯選項(xiàng):
總是使用/W4警告級(jí)別
在調(diào)試版本里總是使用/GZ編譯選項(xiàng),用來(lái)發(fā)現(xiàn)在Release版本中才有的錯(cuò)誤
沒(méi)有警告的編譯:保證在編譯后沒(méi)有任何警告,但是在消除警告前要進(jìn)行仔細(xì)檢查
調(diào)試方法:
1、使用 Assert(原則:盡量簡(jiǎn)單)
assert只在debug下生效,release下不會(huì)被編譯。
例子:
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來(lái)檢測(cè)返回值,通過(guò)得到錯(cuò)誤代碼來(lái)分析錯(cuò)誤原因
5、把錯(cuò)誤信息記錄到文件中
異常處理
程序設(shè)計(jì)時(shí)一定要考慮到異常如何處理,當(dāng)錯(cuò)誤發(fā)生后,不應(yīng)簡(jiǎn)單的報(bào)告錯(cuò)誤并退出程序,應(yīng)當(dāng)盡可能的想辦法恢復(fù)到出錯(cuò)前的狀態(tài)或者讓程序從頭開始運(yùn)行,并且對(duì)于某些錯(cuò)誤,應(yīng)該能夠容錯(cuò),即允許錯(cuò)誤的存在,但是程序還是能夠正常完成任務(wù)。
調(diào)試技巧
1、VC++中F5進(jìn)行調(diào)試運(yùn)行
a)、在output Debug窗口中可以看到用TRACE打印的信息
b)、 Call Stack窗口中能看到程序的調(diào)用堆棧
2、當(dāng)Debug版本運(yùn)行時(shí)發(fā)生崩潰,選擇retry進(jìn)行調(diào)試,通過(guò)看Call Stack分析出錯(cuò)的位置及原因
3、使用映射文件調(diào)試
a)、創(chuàng)建映射文件:Project settings中l(wèi)ink項(xiàng),選中Generate mapfile,輸出程序代碼地址:/MAPINFO: LINES,得到引出序號(hào):/MAPINFO: EXPORTS。
b)、程序發(fā)布時(shí),應(yīng)該把所有模塊的映射文件都存檔。
c)、查看映射文件:見” 通過(guò)崩潰地址找出源代碼的出錯(cuò)行”文件。
4、可以調(diào)試的Release版本
Project settings中C++項(xiàng)的Debug Info選擇為Program Database,Link項(xiàng)的Debug中選擇Debug Info和Microsoft format。
5、查看API的錯(cuò)誤碼,在watch窗口輸入@err可以查看或者@err,hr,其中”,hr”表示錯(cuò)誤碼的說(shuō)明。
6、Set Next Statement:該功能可以直接跳轉(zhuǎn)到指定的代碼行執(zhí)行,一般用來(lái)測(cè)試異常處理的代碼。
7、調(diào)試內(nèi)存變量的變化:當(dāng)內(nèi)存發(fā)生變化時(shí)停下來(lái)。
常見錯(cuò)誤
1、在函數(shù)返回的時(shí)候程序崩潰的原因
a)、寫自動(dòng)變量越界
b)、函數(shù)原型不匹配
2、MFC
a)、使用錯(cuò)誤的函數(shù)原型處理用戶定義消息
正確的函數(shù)原型為:
afx_msg LRESULT OnMyMessage(WPARAM wParam,LPARAM lParam);3、謹(jǐn)慎使用TerminateThread:使用TerminateThread會(huì)造成資源泄漏,不到萬(wàn)不得已,不要使用。
4、使用_beginthreadex,不要使用Create Thread來(lái)常見線程。
參考資料:
《Windows程序調(diào)試》
功能強(qiáng)大的vc6調(diào)試器
作者:yy2better
要成為一位優(yōu)秀的軟件工程師,調(diào)試能力必不可缺。本文將較詳細(xì)介紹VC6調(diào)試器的主要用法。
windows平臺(tái)的調(diào)試器主要分為兩大類:
1 用戶模式(user-mode)調(diào)試器:它們都基于win32 Debugging API,有使用方便的界面,主要用于調(diào)試用戶模式下的應(yīng)用程序。這類調(diào)試器包括Visual C++調(diào)試器、WinDBG、BoundChecker、Borland C++ Builder調(diào)試器、NTSD等。
2 內(nèi)核模式(kernel-mode)調(diào)試器:內(nèi)核調(diào)試器位于CPU和操作系統(tǒng)之間,一旦啟動(dòng),操作系統(tǒng)也會(huì)中止運(yùn)行,主要用于調(diào)試驅(qū)動(dòng)程序或用戶模式調(diào)試器不易調(diào)試的程序。這類調(diào)試器包括WDEB386、WinDBG和softice等。其中WinDBG和softice也可以調(diào)試用戶模式代碼。
國(guó)外一位調(diào)試高手曾說(shuō),他70%調(diào)試時(shí)間是在用VC++,其余時(shí)間是使用WinDBG和softice。畢竟,調(diào)試用戶模式代碼,VC6調(diào)試器的效率是非常高的。因此,我將首先在本篇介紹VC6調(diào)試器的主要用法,其他調(diào)試器的用法及一些調(diào)試技能在后續(xù)文章中闡述。
一 位置斷點(diǎn)(Location Breakpoint)
大家最常用的斷點(diǎn)是普通的位置斷點(diǎn),在源程序的某一行按F9就設(shè)置了一個(gè)位置斷點(diǎn)。但對(duì)于很多問(wèn)題,這種樸素的斷點(diǎn)作用有限。譬如下面這段代碼:
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í)行此函數(shù),程序崩潰于E行,發(fā)現(xiàn)此時(shí)tmp為0,假設(shè)tmp本不應(yīng)該為0,怎么這個(gè)時(shí)候?yàn)?呢?所以最好能夠跟蹤此次循環(huán)時(shí)DoSome函數(shù)是如何運(yùn)行的,但由于是在循環(huán)體內(nèi),如果在E行設(shè)置斷點(diǎn),可能需要按F5(GO)許多次。這樣手要不停的按,很痛苦。使用VC6斷點(diǎn)修飾條件就可以輕易解決此問(wèn)題。步驟如下。
1 Ctrl+B打開斷點(diǎn)設(shè)置框,如下圖:
Figure 1設(shè)置高級(jí)位置斷點(diǎn)
2 然后選擇D行所在的斷點(diǎn),然后點(diǎn)擊condition按鈕,在彈出對(duì)話框的最下面一個(gè)編輯框中輸入一個(gè)很大數(shù)目,具體視應(yīng)用而定,這里1000就夠了。
3 按F5重新運(yùn)行程序,程序中斷。Ctrl+B打開斷點(diǎn)框,發(fā)現(xiàn)此斷點(diǎn)后跟隨一串說(shuō)明:...487 times remaining。意思是還剩下487次沒(méi)有執(zhí)行,那就是說(shuō)執(zhí)行到513(1000-487)次時(shí)候出錯(cuò)的。因此,我們按步驟2所講,更改此斷點(diǎn)的skip次數(shù),將1000改為513。
4 再次重新運(yùn)行程序,程序執(zhí)行了513次循環(huán),然后自動(dòng)停在斷點(diǎn)處。這時(shí),我們就可以仔細(xì)查看DoSome是如何返回0的。這樣,你就避免了手指的痛苦,節(jié)省了時(shí)間。
再看位置斷點(diǎn)其他修飾條件。如Figure 1所示,在“Enter the expression to be evaluated:”下面,可以輸入一些條件,當(dāng)這些條件滿足時(shí),斷點(diǎn)才啟動(dòng)。譬如,剛才的程序,我們需要i為100時(shí)程序停下來(lái),我們就可以輸入在編輯框中輸入“i==100”。
另外,如果在此編輯框中如果只輸入變量名稱,則變量發(fā)生改變時(shí),斷點(diǎn)才會(huì)啟動(dòng)。這對(duì)檢測(cè)一個(gè)變量何時(shí)被修改很方便,特別對(duì)一些大程序。
用好位置斷點(diǎn)的修飾條件,可以大大方便解決某些問(wèn)題。
二 數(shù)據(jù)斷點(diǎn)(Data Breakpoint)
軟件調(diào)試過(guò)程中,有時(shí)會(huì)發(fā)現(xiàn)一些數(shù)據(jù)會(huì)莫名其妙的被修改掉(如一些數(shù)組的越界寫導(dǎo)致覆蓋了另外的變量),找出何處代碼導(dǎo)致這塊內(nèi)存被更改是一件棘手的事情(如果沒(méi)有調(diào)試器的幫助)。恰當(dāng)運(yùn)用數(shù)據(jù)斷點(diǎn)可以快速幫你定位何時(shí)何處這個(gè)數(shù)據(jù)被修改。譬如下面一段程序:
#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何時(shí)被修改呢?因?yàn)闆](méi)有明顯的修改szName1代碼。我們可以首先在A行設(shè)置普通斷點(diǎn),F(xiàn)5運(yùn)行程序,程序停在A行。然后我們?cè)僭O(shè)置一個(gè)數(shù)據(jù)斷點(diǎn)。如下圖:
Figure 2 數(shù)據(jù)斷點(diǎn)
F5繼續(xù)運(yùn)行,程序停在B行,說(shuō)明B處代碼修改了szName1。B處明明沒(méi)有修改szName1呀?但調(diào)試器指明是這一行,一般不會(huì)錯(cuò),所以還是靜下心來(lái)看看程序,哦,你發(fā)現(xiàn)了:szName2只有4個(gè)字節(jié),而strcpy了7個(gè)字節(jié),所以覆寫了szName1。
數(shù)據(jù)斷點(diǎn)不只是對(duì)變量改變有效,還可以設(shè)置變量是否等于某個(gè)值。譬如,你可以將Figure 2中紅圈處改為條件”szName2[0]==''''y''''“,那么當(dāng)szName2第一個(gè)字符為y時(shí)斷點(diǎn)就會(huì)啟動(dòng)。
可以看出,數(shù)據(jù)斷點(diǎn)相對(duì)位置斷點(diǎn)一個(gè)很大的區(qū)別是不用明確指明在哪一行代碼設(shè)置斷點(diǎn)。
三 其他
1 在call stack窗口中設(shè)置斷點(diǎn),選擇某個(gè)函數(shù),按F9設(shè)置一個(gè)斷點(diǎn)。這樣可以從深層次的函數(shù)調(diào)用中迅速返回到需要的函數(shù)。
2 Set Next StateMent命令(debug過(guò)程中,右鍵菜單中的命令)
此命令的作用是將程序的指令指針(EIP)指向不同的代碼行。譬如,你正在調(diào)試上面那段代碼,運(yùn)行在A行,但你不愿意運(yùn)行B行和C行代碼,這時(shí),你就可以在D行,右鍵,然后“Set Next StateMent”。調(diào)試器就不會(huì)執(zhí)行B、C行。只要在同一函數(shù)內(nèi),此指令就可以隨意跳前或跳后執(zhí)行。靈活使用此功能可以大量節(jié)省調(diào)試時(shí)間。
3 watch窗口
watch窗口支持豐富的數(shù)據(jù)格式化功能。如輸入0x65,u,則在右欄顯示101。
實(shí)時(shí)顯示windows API調(diào)用的錯(cuò)誤:在左欄輸入@err,hr。
在watch窗口中調(diào)用函數(shù)。提醒一下,調(diào)用完函數(shù)后馬上在watch窗口中清除它,否則,單步調(diào)試時(shí)每一步調(diào)試器都會(huì)調(diào)用此函數(shù)。
4 messages斷點(diǎn)不怎么實(shí)用?;旧峡梢杂们懊嬷v述的斷點(diǎn)代替。
總結(jié)
調(diào)試最重要的還是你要思考,要猜測(cè)你的程序可能出錯(cuò)的地方,然后運(yùn)用你的調(diào)試器來(lái)證實(shí)你的猜測(cè)。而熟練使用上面這些技巧無(wú)疑會(huì)加快這個(gè)過(guò)程。最后,大家如果有關(guān)于調(diào)試方面的問(wèn)題,我樂(lè)意參與探討。
VC調(diào)試入門
作者:阿榮
概述
調(diào)試是一個(gè)程序員最基本的技能,其重要性甚至超過(guò)學(xué)習(xí)一門語(yǔ)言。不會(huì)調(diào)試的程序員就意味著他即使會(huì)一門語(yǔ)言,卻不能編制出任何好的軟件。
這里我簡(jiǎn)要的根據(jù)自己的經(jīng)驗(yàn)列出調(diào)試中比較常用的技巧,希望對(duì)大家有用。
本文約定,在選擇菜單時(shí),通過(guò)/表示分級(jí)菜單,例如File/Open表示頂級(jí)菜單File的子菜單Open。
設(shè)置
為了調(diào)試一個(gè)程序,首先必須使程序中包含調(diào)試信息。一般情況下,一個(gè)從AppWizard創(chuàng)建的工程中包含的Debug Configuration自動(dòng)包含調(diào)試信息,但是是不是Debug版本并不是程序包含調(diào)試信息的決定因素,程序設(shè)計(jì)者可以在任意的Configuration中增加調(diào)試信息,包括Release版本。
為了增加調(diào)試信息,可以按照下述步驟進(jìn)行:
打開Project settings對(duì)話框(可以通過(guò)快捷鍵ALT+F7打開,也可以通過(guò)IDE菜單Project/Settings打開)
選擇C/C++頁(yè),Category中選擇general ,則出現(xiàn)一個(gè)Debug Info下拉列表框,可供選擇的調(diào)試信息 方式包括:
命令行 Project settings 說(shuō)明
無(wú) None 沒(méi)有調(diào)試信息
/Zd Line Numbers Only 目標(biāo)文件或者可執(zhí)行文件中只包含全局和導(dǎo)出符號(hào)以及代碼行信息,不包含符號(hào)調(diào)試信息
/Z7 C 7.0- Compatible 目標(biāo)文件或者可執(zhí)行文件中包含行號(hào)和所有符號(hào)調(diào)試信息,包括變量名及類型,函數(shù)及原型等
/Zi Program Database 創(chuàng)建一個(gè)程序庫(kù)(PDB),包括類型信息和符號(hào)調(diào)試信息。
/ZI Program Database for Edit and Continue 除了前面/Zi的功能外,這個(gè)選項(xiàng)允許對(duì)代碼進(jìn)行調(diào)試過(guò)程中的修改和繼續(xù)執(zhí)行。這個(gè)選項(xiàng)同時(shí)使#pragma設(shè)置的優(yōu)化功能無(wú)效
選擇Link頁(yè),選中復(fù)選框"Generate Debug Info",這個(gè)選項(xiàng)將使連接器把調(diào)試信息寫進(jìn)可執(zhí)行文件和DLL
如果C/C++頁(yè)中設(shè)置了Program Database以上的選項(xiàng),則Link incrementally可以選擇。選中這個(gè)選項(xiàng),將使程序可以在上一次編譯的基礎(chǔ)上被編譯(即增量編譯),而不必每次都從頭開始編譯。
斷點(diǎn)
斷點(diǎn)是調(diào)試器設(shè)置的一個(gè)代碼位置。當(dāng)程序運(yùn)行到斷點(diǎn)時(shí),程序中斷執(zhí)行,回到調(diào)試器。斷點(diǎn)是 最常用的技巧。調(diào)試時(shí),只有設(shè)置了斷點(diǎn)并使程序回到調(diào)試器,才能對(duì)程序進(jìn)行在線調(diào)試。
設(shè)置斷點(diǎn):可以通過(guò)下述方法設(shè)置一個(gè)斷點(diǎn)。首先把光標(biāo)移動(dòng)到需要設(shè)置斷點(diǎn)的代碼行上,然后
按F9快捷鍵
彈出Breakpoints對(duì)話框,方法是按快捷鍵CTRL+B或ALT+F9,或者通過(guò)菜單Edit/Breakpoints打開。打開后點(diǎn)擊Break at編輯框的右側(cè)的箭頭,選擇 合適的位置信息。一般情況下,直接選擇line xxx就足夠了,如果想設(shè)置不是當(dāng)前位置的斷點(diǎn),可以選擇Advanced,然后填寫函數(shù)、行號(hào)和可執(zhí)行文件信息。
去掉斷點(diǎn):把光標(biāo)移動(dòng)到給定斷點(diǎn)所在的行,再次按F9就可以取消斷點(diǎn)。同前面所述,打開Breakpoints對(duì)話框后,也可以按照界面提示去掉斷點(diǎn)。
條件斷點(diǎn):可以為斷點(diǎn)設(shè)置一個(gè)條件,這樣的斷點(diǎn)稱為條件斷點(diǎn)。對(duì)于新加的斷點(diǎn),可以單擊Conditions按鈕,為斷點(diǎn)設(shè)置一個(gè)表達(dá)式。當(dāng)這個(gè)表達(dá)式發(fā)生改變時(shí),程序就 被中斷。底下設(shè)置包括“觀察數(shù)組或者結(jié)構(gòu)的元素個(gè)數(shù)”,似乎可以設(shè)置一個(gè)指針?biāo)赶虻膬?nèi)存區(qū)的大小,但是我設(shè)置一個(gè)比較的值但是改動(dòng) 范圍之外的內(nèi)存區(qū)似乎也導(dǎo)致斷點(diǎn)起效。最后一個(gè)設(shè)置可以讓程序先執(zhí)行多少次然后才到達(dá)斷點(diǎn)。
數(shù)據(jù)斷點(diǎn):數(shù)據(jù)斷點(diǎn)只能在Breakpoints對(duì)話框中設(shè)置。選擇“Data”頁(yè),就顯示了設(shè)置數(shù)據(jù)斷點(diǎn)的對(duì)話框。在編輯框中輸入一個(gè)表達(dá)式,當(dāng)這個(gè) 表達(dá)式的值發(fā)生變化時(shí),數(shù)據(jù)斷點(diǎn)就到達(dá)。一般情況下,這個(gè)表達(dá)式應(yīng)該由運(yùn)算符和全局變量構(gòu)成,例如:在編輯框中輸入 g_bFlag這個(gè)全局變量的名字,那么當(dāng)程序中有g(shù)_bFlag= !g_bFlag時(shí),程序就將停在這個(gè)語(yǔ)句處。
消息斷點(diǎn):VC也支持對(duì)Windows消息進(jìn)行截獲。他有兩種方式進(jìn)行截獲:窗口消息處理函數(shù)和特定消息中斷。
在Breakpoints對(duì)話框中選擇Messages頁(yè),就可以設(shè)置消息斷點(diǎn)。如果在上面那個(gè)對(duì)話框中寫入消息處理函數(shù)的名字,那么 每次消息被這個(gè)函數(shù)處理,斷點(diǎn)就到達(dá)(我覺(jué)得如果采用普通斷點(diǎn)在這個(gè)函數(shù)中截獲,效果應(yīng)該一樣)。如果在底下的下拉 列表框選擇一個(gè)消息,則每次這種消息到達(dá),程序就中斷。
值
Watch
VC支持查看變量、表達(dá)式和內(nèi)存的值。所有這些觀察都必須是在斷點(diǎn)中斷的情況下進(jìn)行。
觀看變量的值最簡(jiǎn)單,當(dāng)斷點(diǎn)到達(dá)時(shí),把光標(biāo)移動(dòng)到這個(gè)變量上,停留一會(huì)就可以看到變量的值。
VC提供一種被成為Watch的機(jī)制來(lái)觀看變量和表達(dá)式的值。在斷點(diǎn)狀態(tài)下,在變量上單擊右鍵,選擇Quick Watch, 就彈出一個(gè)對(duì)話框,顯示這個(gè)變量的值。
單擊Debug工具條上的Watch按鈕,就出現(xiàn)一個(gè)Watch視圖(Watch1,Watch2,Watch3,Watch4),在該視圖中輸入變量或者表達(dá)式,就可以觀察 變量或者表達(dá)式的值。注意:這個(gè)表達(dá)式不能有副作用,例如++運(yùn)算符絕對(duì)禁止用于這個(gè)表達(dá)式中,因?yàn)檫@個(gè)運(yùn)算符將修改變量的值,導(dǎo)致 軟件的邏輯被破壞。
Memory
由于指針指向的數(shù)組,Watch只能顯示第一個(gè)元素的值。為了顯示數(shù)組的后續(xù)內(nèi)容,或者要顯示一片內(nèi)存的內(nèi)容,可以使用memory功能。在 Debug工具條上點(diǎn)memory按鈕,就彈出一個(gè)對(duì)話框,在其中輸入地址,就可以顯示該地址指向的內(nèi)存的內(nèi)容。
Varibles
Debug工具條上的Varibles按鈕彈出一個(gè)框,顯示所有當(dāng)前執(zhí)行上下文中可見的變量的值。特別是當(dāng)前指令涉及的變量,以紅色顯示。
寄存器
Debug工具條上的Reigsters按鈕彈出一個(gè)框,顯示當(dāng)前的所有寄存器的值。
進(jìn)程控制
VC允許被中斷的程序繼續(xù)運(yùn)行、單步運(yùn)行和運(yùn)行到指定光標(biāo)處,分別對(duì)應(yīng)快捷鍵F5、F10/F11和CTRL+F10。各個(gè)快捷鍵功能如下:
快捷鍵 說(shuō)明
F5 繼續(xù)運(yùn)行
F10 單步,如果涉及到子函數(shù),不進(jìn)入子函數(shù)內(nèi)部
F11 單步,如果涉及到子函數(shù),進(jìn)入子函數(shù)內(nèi)部
CTRL+F10 運(yùn)行到當(dāng)前光標(biāo)處。
Call Stack
調(diào)用堆棧反映了當(dāng)前斷點(diǎn)處函數(shù)是被那些函數(shù)按照什么順序調(diào)用的。單擊Debug工具條上的Call stack就顯示Call Stack對(duì)話框。在CallStack對(duì)話框中顯示了一個(gè)調(diào)用系列,最上面的是當(dāng)前函數(shù),往下依次是調(diào)用函數(shù)的上級(jí)函數(shù)。單擊這些函數(shù)名可以跳到對(duì)應(yīng)的函數(shù)中去。
其他調(diào)試手段
系統(tǒng)提供一系列特殊的函數(shù)或者宏來(lái)處理Debug版本相關(guān)的信息,如下:
宏名/函數(shù)名 說(shuō)明
TRACE 使用方法和printf完全一致,他在output框中輸出調(diào)試信息
ASSERT 它接收一個(gè)表達(dá)式,如果這個(gè)表達(dá)式為TRUE,則無(wú)動(dòng)作,否則中斷當(dāng)前程序執(zhí)行。對(duì)于系統(tǒng)中出現(xiàn)這個(gè)宏 導(dǎo)致的中斷,應(yīng)該認(rèn)為你的函數(shù)調(diào)用未能滿足系統(tǒng)的調(diào)用此函數(shù)的前提條件。例如,對(duì)于一個(gè)還沒(méi)有創(chuàng)建的窗口調(diào)用SetWindowText等。
VERIFY 和ASSERT功能類似,所不同的是,在Release版本中,ASSERT不計(jì)算輸入的表達(dá)式的值,而VERIFY計(jì)算表達(dá)式的值。
關(guān)注
一個(gè)好的程序員不應(yīng)該把所有的判斷交給編譯器和調(diào)試器,應(yīng)該在程序中自己加以程序保護(hù)和錯(cuò)誤定位,具體措施包括:
對(duì)于所有有返回值的函數(shù),都應(yīng)該檢查返回值,除非你確信這個(gè)函數(shù)調(diào)用絕對(duì)不會(huì)出錯(cuò),或者不關(guān)心它是否出錯(cuò)。
一些函數(shù)返回錯(cuò)誤,需要用其他函數(shù)獲得錯(cuò)誤的具體信息。例如accept返回INVALID_SOCKET表示accept失敗,為了查明 具體的失敗原因,應(yīng)該立刻用WSAGetLastError獲得錯(cuò)誤碼,并針對(duì)性的解決問(wèn)題。
有些函數(shù)通過(guò)異常機(jī)制拋出錯(cuò)誤,應(yīng)該用TRY-CATCH語(yǔ)句來(lái)檢查錯(cuò)誤
程序員對(duì)于能處理的錯(cuò)誤,應(yīng)該自己在底層處理,對(duì)于不能處理的,應(yīng)該報(bào)告給用戶讓他們決定怎么處理。如果程序出了異常, 卻不對(duì)返回值和其他機(jī)制返回的錯(cuò)誤信息進(jìn)行判斷,只能是加大了找錯(cuò)誤的難度。
另外:VC中要編制程序不應(yīng)該一開始就寫cpp/h文件,而應(yīng)該首先創(chuàng)建一個(gè)合適的工程。因?yàn)橹挥羞@樣,VC才能選擇合適的編譯、連接 選項(xiàng)。對(duì)于加入到工程中的cpp文件,應(yīng)該檢查是否在第一行顯式的包含stdafx.h頭文件,這是Microsoft Visual Studio為了加快編譯 速度而設(shè)置的預(yù)編譯頭文件。在這個(gè)#include "stdafx.h"行前面的所有代碼將被忽略,所以其他頭文件應(yīng)該在這一行后面被包含。
對(duì)于.c文件,由于不能包含stdafx.h,因此可以通過(guò)Project settings把它的預(yù)編譯頭設(shè)置為“不使用”,方法是:
彈出Project settings對(duì)話框
選擇C/C++
Category選擇Precompilation Header
選擇不使用預(yù)編譯頭。
關(guān)于調(diào)試時(shí)輸出的字符串信息
作者:①塌糊涂
下載源代碼
使用工具:VC6.0,IDA
當(dāng)我們要在程序中輸出調(diào)試信息時(shí),常常以字符串的形式來(lái)輸出,例如:
printf("Some debug information here!\n");這段代碼在Debug和Release版下都輸出調(diào)試信息,這不是我們所要的,一般地大家都會(huì)添加
預(yù)編譯指令,如下所示:
#if _DEBUG
printf("Some debug information here!\n");
#endif這樣就達(dá)到了在Debug版里程序輸出調(diào)試信息,在Release版下不輸出調(diào)試信息的目的。(在Release版里
連printf函數(shù)都沒(méi)有調(diào)用)可如果要在程序里的許多地方輸出調(diào)試信息,若采用上面的方式會(huì)很麻煩;
(至于為什么麻煩,可能就是不愿多敲幾次鍵盤吧,呵呵。。。)
于是大家都想到寫個(gè)輸出函數(shù),代碼如下:
void printInfo(char *strInfo)
{
#if _DEBUG
printf(strInfo);
#endif
}注:該函數(shù)只是演示用的,很簡(jiǎn)單,沒(méi)有其他檢查字符串功能。
在要輸出調(diào)試信息的地方,調(diào)用如下語(yǔ)句就行:
printInfo("Some debug information here!\n");
確實(shí),在Debug模式下運(yùn)行該程序,則輸出如下信息:
Some debug information here!在Release模式下,則沒(méi)輸出什么信息;
我們往往在這個(gè)時(shí)候認(rèn)為一切都OK了;如果你認(rèn)為是,就沒(méi)必要往下看了;呵呵。。。
雖然在Release版下運(yùn)行程序沒(méi)有輸出調(diào)試信息來(lái),可這些調(diào)試信息卻留在了二進(jìn)制的可執(zhí)行文件里;
我們可以用IDA來(lái)打開該Release版的可執(zhí)行文件,看到如圖一所示的信息:
圖一:IDA反匯編后的main函數(shù)
注:該函數(shù)就是main函數(shù)
可見調(diào)試信息字符串(“Some debug information here!\n”)確實(shí)存在于Release版的可執(zhí)行文件里;
我們當(dāng)然不希望別人看到這些調(diào)試信息,那有沒(méi)有辦法來(lái)防止該調(diào)試信息被編譯進(jìn)Release版的可執(zhí)行文件里呢?
辦法是有的,這里來(lái)描述2個(gè)方法。
辦法一:
定義如下宏:
#if _DEBUG
#define _D(str) str
#else
#define _D(str) NULL
#endif此時(shí)輸出語(yǔ)句變?yōu)椋?/p>
printInfo(_D("Some debug information here!\n"));
在Debug模式下運(yùn)行程序,依然輸出調(diào)試信息:
“Some debug information here!”;在Release下,則什么都不輸出,此時(shí)我們用IDA看一下Release版的二進(jìn)制文件,則沒(méi)有發(fā)現(xiàn)該調(diào)試信息字符串。
如圖二示:
圖二:IDA反匯編后的main函數(shù)
方法二:
定義如下宏:
#if _DEBUG
void printInfo(char *strInfo)
{
printf(strInfo);
}
#else
#define printInfo(str)
#endif注意:該宏把函數(shù)printInfo的定義也放進(jìn)去了;
在Debug模式下運(yùn)行程序,也同樣輸出調(diào)試信息:
“Some debug information here!”;在Release下,也什么都不輸出,此時(shí)我們用IDA看一下Release版的二進(jìn)制文件,也沒(méi)有發(fā)現(xiàn)該調(diào)試信息字符串。
如圖三示:
圖三:IDA反匯編后的main函數(shù)
既然方法一和方法二都能實(shí)現(xiàn)同樣的功能,那究竟那個(gè)方法好呢?
方法一和方法二確實(shí)都沒(méi)在可執(zhí)行文件里留下調(diào)試信息,比較一下圖二和圖三,我們不難發(fā)現(xiàn):
圖二當(dāng)中多了一個(gè)函數(shù)調(diào)用 call nullsub_1,該函數(shù)就是printInfo,雖然該函數(shù)什么都不做,
但它卻調(diào)用了,我們一般也不希望該函數(shù)調(diào)用,所以方法一中多了一個(gè)函數(shù)調(diào)用,增加了開銷,
而方法二當(dāng)中卻沒(méi)有調(diào)用該函數(shù)。
個(gè)人認(rèn)為方法二較好。
結(jié)束語(yǔ):
若要轉(zhuǎn)載該文章,請(qǐng)保持原文章的完整性,謝謝!
文中如有不妥之處,請(qǐng)指正,謝謝!
E-mail:grapeky@etang.com
調(diào)用規(guī)范與可變參數(shù)表
作者:阿半
語(yǔ)言調(diào)用規(guī)范是指進(jìn)行一次函數(shù)調(diào)用所采用的傳遞參數(shù)的方法,返回值的處理以及調(diào)用堆棧的清理。Microsoft C/C++ 語(yǔ)言中采用了五種調(diào)用規(guī)范,分別是__cdecl, __stdcall, __fastcall,thiscall和nake每一中調(diào)用規(guī)范都是利用eax作為返回值,如果函數(shù)返回值是64位的,則利用edx:eax對(duì)來(lái)返回值。Nake調(diào)用規(guī)范非常的靈活,足以獨(dú)立的一篇文章描述,這里就不再描述nake調(diào)用規(guī)范。下表列出了前面四種規(guī)范調(diào)用的特點(diǎn):
關(guān)鍵字 堆棧清理者 參數(shù)傳遞順序
__cdecl 調(diào)用者 從右至左
__stdcall 被調(diào)用者 從右至左
__fastcall 被調(diào)用者 從右至左,前兩個(gè)參數(shù)由寄存器ecx,edx傳遞
thiscall 被調(diào)用者或者調(diào)用者 從右至左
__cdecl 最大好處在于由于是調(diào)用者清理?xiàng)?,它可以處理可變參?shù),缺點(diǎn)則在于它增加了程序的大小,因?yàn)樵诿總€(gè)調(diào)用返回的時(shí)候,需要多執(zhí)行一條清理?xiàng)5闹噶睢?br> __stdcall 是在windows程序設(shè)計(jì)中出現(xiàn)的最多的調(diào)用規(guī)則,所有的不可變參數(shù)的API調(diào)用都使用這個(gè)規(guī)則。
__fastcall 在windows內(nèi)核設(shè)計(jì)中被廣泛的使用,由于兩個(gè)參數(shù)由寄存器直接傳遞,采用這種規(guī)則的函數(shù)效率要比以上兩種規(guī)則高。
thiscall是C++成員函數(shù)的默認(rèn)調(diào)用規(guī)范,編譯期間,這種調(diào)用會(huì)根據(jù)函數(shù)是否支持可變參數(shù)表來(lái)決定采用什么方式清理堆棧。如果成員函數(shù)不支持可變參數(shù),那么它就是用參數(shù)入棧,ecx保存this指針的方式進(jìn)行調(diào)用,如果成員函數(shù)支持可變參數(shù),那么它的調(diào)用和__cdecl類似,唯一不同的是將this指針最后壓入棧中進(jìn)行傳遞。
調(diào)用者和被調(diào)用者必須采用同樣的規(guī)則才能保證程序的正常執(zhí)行,曾經(jīng)看到很多程序員犯的錯(cuò)誤就是由于調(diào)用規(guī)范的不一樣,致使程序異常,比如:
DWORD ThreadFunc(LPVOID lpParam)
{
//…
}
CreateThread(..,(LPTHREAD_START_ROUTINE)ThreadFunc, …); 如果在編譯期間沒(méi)有指定編譯選項(xiàng)/Gz(指定未指明調(diào)用規(guī)范的函數(shù)采用__stdcall方式),那么編譯器自動(dòng)將ThreadFunc處理成__cdecl調(diào)用規(guī)范(/Gd),這樣可能在線程開始的時(shí)候正常執(zhí)行,然而退出的時(shí)候由于堆棧沒(méi)有正常清理,造成訪問(wèn)違例或者非法指令錯(cuò)誤。
以上說(shuō)了很多清理?xiàng)5膯?wèn)題,那么為什么清理?xiàng):苤匾?。堆棧是線程相關(guān)的,也就是說(shuō)每一個(gè)線程含有一個(gè)堆棧,這個(gè)堆棧上保存了局部變量,調(diào)用返回地址等很多線程相關(guān)的數(shù)據(jù),這也是為什么獨(dú)立運(yùn)行的線程可以調(diào)用同樣一個(gè)函數(shù)而互不干擾的原因。堆棧的特點(diǎn)恐怕大家已經(jīng)非常熟悉了,那么根據(jù)上面的每一種調(diào)用,我給出一個(gè)簡(jiǎn)單的圖示來(lái)說(shuō)明清理堆棧的重要性,以及為什么上面的例子代碼會(huì)出錯(cuò)。
圖一 這是線程堆棧在運(yùn)行的時(shí)候的樣子
調(diào)用前和后esp的差值中間包含了函數(shù)參數(shù)表,返回地址這樣的重要信息,舉個(gè)簡(jiǎn)單的調(diào)用例子.假設(shè)有某個(gè)函數(shù)定義是這樣的:
Int __cdecl func(void* p);再假設(shè)esp調(diào)用函數(shù)前的數(shù)值為0x1234,那么在進(jìn)入這個(gè)函數(shù)體內(nèi)看到的堆棧是這樣的:
122C 1230 1234
Next p 這里的next指調(diào)用函數(shù)后的下一條指令的位置。調(diào)用函數(shù)的匯編碼:
Push p
Call func
Add esp,4 《--注意這里,由于是cdecl調(diào)用,需要調(diào)用者清棧。而一個(gè)__stdcall調(diào)用的匯編碼:
Push p
Call func 這里沒(méi)有了add esp,4這個(gè)指令,因?yàn)樵趂unc函數(shù)返回的時(shí)候自己將esp已經(jīng)復(fù)原了。再來(lái)看剛才舉的錯(cuò)誤的例子,由于強(qiáng)制轉(zhuǎn)換的作用,線程開始函數(shù)被設(shè)置成了stdcall調(diào)用,而實(shí)際的線程函數(shù)被編譯后,并沒(méi)有執(zhí)行堆棧的清理工作,線程函數(shù)返回的時(shí)候,由于堆棧的不正確,當(dāng)然會(huì)發(fā)生錯(cuò)誤。修改這個(gè)bug的方法只要在線程函數(shù)的定義前把__cdecl改成_stdcall即可。
有了上面的例子做基礎(chǔ)來(lái)理解可變參數(shù)表就簡(jiǎn)單的多了,由于各種調(diào)用規(guī)范的限定,致使只有__cdecl調(diào)用規(guī)范可以采用可變參數(shù)表。先來(lái)看看可變參數(shù)表的定義(可以參考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* ?沒(méi)錯(cuò),這實(shí)際是用來(lái)定義了一個(gè)指針,指針的sizeof()就是操作系統(tǒng)可訪問(wèn)的地址空間的大小,也就是CPU相關(guān)的字長(zhǎng)。_INTSIZEOF宏很簡(jiǎn)單,就是用來(lái)將數(shù)據(jù)以n的數(shù)據(jù)大小對(duì)齊。va_start宏有點(diǎn)模糊,可是如果你看懂了上面的堆棧數(shù)據(jù)結(jié)構(gòu),那么顯然它就是獲得最后一個(gè)固定參數(shù)的地址,也就是堆棧上的地址,va_arg先使得ap指向下一個(gè)參數(shù),然后取得當(dāng)前參數(shù)的值(注意,這個(gè)值正是堆棧上的值),va_end使得取參數(shù)過(guò)程結(jié)束。
這幾個(gè)宏完成的動(dòng)作很簡(jiǎn)單了,實(shí)際就是取得可變參數(shù)表在堆棧上的起始位置,然后根據(jù)參數(shù)類型,依次從堆棧上取出每一個(gè)參數(shù)。
本文簡(jiǎn)單的介紹了微軟C/C++支持的調(diào)用類型,結(jié)合實(shí)例描述了規(guī)范的實(shí)際應(yīng)用,最后根據(jù)CRT提供的源代碼分析了可變參數(shù)表的實(shí)現(xiàn)。
僅通過(guò)崩潰地址找出源代碼的出錯(cuò)行
作者:老羅
提交者:eastvc 發(fā)布日期:2003-10-23 9:16:11
原文出處:http://www.luocong.com/articles/show_article.asp?Article_ID=29
作為程序員,我們平時(shí)最擔(dān)心見到的事情是什么?是內(nèi)存泄漏?是界面不好看?……錯(cuò)啦!我相信我的看法是不會(huì)有人反對(duì)的——那就是,程序發(fā)生了崩潰!
“該程序執(zhí)行了非法操作,即將關(guān)閉。請(qǐng)與你的軟件供應(yīng)商聯(lián)系。”,呵呵,這句 M$ 的“名言”,恐怕就是程序員最擔(dān)心見到的東西了。有的時(shí)候,自己的程序在自己的機(jī)器上運(yùn)行得好好的,但是到了別人的機(jī)器上就崩潰了;有時(shí)自己在編寫和測(cè)試的過(guò)程中就莫名其妙地遇到了非法操作,但是卻無(wú)法確定到底是源代碼中的哪行引起的……是不是很痛苦呢?不要緊,本文可以幫助你走出這種困境,甚至你從此之后可以自豪地要求用戶把崩潰地址告訴你,然后你就可以精確地定位到源代碼中出錯(cuò)的那行了。(很神奇吧?呵呵。)
首先我必須強(qiáng)調(diào)的是,本方法可以在目前市面上任意一款編譯器上面使用。但是我只熟悉 M$ 的 VC 和 MASM ,因此后面的部分只介紹如何在這兩個(gè)編譯器中實(shí)現(xiàn),請(qǐng)讀者自行融會(huì)貫通,掌握在別的編譯器上使用的方法。
Well,廢話說(shuō)完了,讓我們開始! :)
首先必須生成程序的 MAP 文件。什么是 MAP 文件?簡(jiǎn)單地講, MAP 文件是程序的全局符號(hào)、源文件和代碼行號(hào)信息的唯一的文本表示方法,它可以在任何地方、任何時(shí)候使用,不需要有額外的程序進(jìn)行支持。而且,這是唯一能找出程序崩潰的地方的救星。
好吧,既然 MAP 文件如此神奇,那么我們應(yīng)該如何生成它呢?在 VC 中,我們可以按下 Alt+F7 ,打開“Project Settings”選項(xiàng)頁(yè),選擇 C/C++ 選項(xiàng)卡,并在最下面的 Project Options 里面輸入:/Zd ,然后要選擇 Link 選項(xiàng)卡,在最下面的 Project Options 里面輸入: /mapinfo:lines 和 /map:PROJECT_NAME.map 。最后按下 F7 來(lái)編譯生成 EXE 可執(zhí)行文件和 MAP 文件。
在 MASM 中,我們要設(shè)置編譯和連接參數(shù),我通常是這樣做的:
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 來(lái)編譯生成 EXE 可執(zhí)行文件和 MAP 文件了。
在此我先解釋一下加入的參數(shù)的含義:
/Zd 表示在編譯的時(shí)候生成行信息
/map[:filename] 表示生成 MAP 文件的路徑和文件名
/mapinfo:lines 表示生成 MAP 文件時(shí),加入行信息
/mapinfo:exports 表示生成 MAP 文件時(shí),加入 exported functions (如果生成的是 DLL 文件,這個(gè)選項(xiàng)就要加上)
OK,通過(guò)上面的步驟,我們已經(jīng)得到了 MAP 文件,那么我們?cè)撊绾卫盟兀?/p>
讓我們從簡(jiǎn)單的實(shí)例入手,請(qǐng)打開你的 VC ,新建這樣一個(gè)文件:
01 //****************************************************************
02 //程序名稱:演示如何通過(guò)崩潰地址找出源代碼的出錯(cuò)行
03 //作者:羅聰
04 //日期:2003-2-7
05 //出處:http://www.luocong.com(老羅的繽紛天地)
06 //本程序會(huì)產(chǎn)生“除0錯(cuò)誤”,以至于會(huì)彈出“非法操作”對(duì)話框。
07 //“除0錯(cuò)誤”只會(huì)在 Debug 版本下產(chǎn)生,本程序?yàn)榱搜菔径M量簡(jiǎn)化。
08 //注意事項(xiàng):如欲轉(zhuǎn)載,請(qǐng)保持本程序的完整,并注明:
09 //轉(zhuǎn)載自“老羅的繽紛天地”(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錯(cuò)誤”,在 Debug 方式下編譯的話,運(yùn)行時(shí)肯定會(huì)產(chǎn)生“非法操作”。好,讓我們運(yùn)行它,果然,“非法操作”對(duì)話框出現(xiàn)了,這時(shí)我們點(diǎn)擊“詳細(xì)信息”按鈕,記錄下產(chǎn)生崩潰的地址——在我的機(jī)器上是 0x0040104a 。
再看看它的 MAP 文件:(由于文件內(nèi)容太長(zhǎng),中間沒(méi)用的部分我進(jìn)行了省略)
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
如果仔細(xì)瀏覽 Rva+Base 這欄,你會(huì)發(fā)現(xiàn)第一個(gè)比崩潰地址 0x0040104a 大的函數(shù)地址是 0x00401070 ,所以在 0x00401070 這個(gè)地址之前的那個(gè)入口就是產(chǎn)生崩潰的函數(shù),也就是這行:
0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo.obj
因此,發(fā)生崩潰的函數(shù)就是 ?Crash@@YAXXZ ,所有以問(wèn)號(hào)開頭的函數(shù)名稱都是 C++ 修飾的名稱。在我們的源程序中,也就是 Crash() 這個(gè)子函數(shù)。
OK,現(xiàn)在我們輕而易舉地便知道了發(fā)生崩潰的函數(shù)名稱,你是不是很興奮呢?呵呵,先別忙,接下來(lái),更厲害的招數(shù)要出場(chǎng)了。
請(qǐng)注意 MAP 文件的最后部分——代碼行信息(Line numbers information),它是以這樣的形式顯示的:
13 0001:00000020
第一個(gè)數(shù)字代表在源代碼中的代碼行號(hào),第二個(gè)數(shù)是該代碼行在所屬的代碼段中的偏移量。
如果要查找代碼行號(hào),需要使用下面的公式做一些十六進(jìn)制的減法運(yùn)算:
崩潰行偏移 = 崩潰地址(Crash Address) - 基地址(ImageBase Address) - 0x1000
為什么要這樣做呢?細(xì)心的朋友可能會(huì)留意到 Rva+Base 這欄了,我們得到的崩潰地址都是由 偏移地址(Rva)+ 基地址(Base) 得來(lái)的,所以在計(jì)算行號(hào)的時(shí)候要把基地址減去,一般情況下,基地址的值是 0x00400000 。另外,由于一般的 PE 文件的代碼段都是從 0x1000 偏移開始的,所以也必須減去 0x1000 。
好了,明白了這點(diǎn),我們就可以來(lái)進(jìn)行小學(xué)減法計(jì)算了:
崩潰行偏移 = 0x0040104a - 0x00400000 - 0x1000 = 0x4a
如果瀏覽 MAP 文件的代碼行信息,會(huì)看到不超過(guò)計(jì)算結(jié)果,但卻最接近的數(shù)是 CrashDemo.cpp 文件中的:
16 0001:00000046
也就是在源代碼中的第 16 行,讓我們來(lái)看看源代碼:
16 i /= j;
哈?。?!果然就是第 16 行??!
興奮嗎?我也一樣! :)
方法已經(jīng)介紹完了,從今以后,我們就可以精確地定位到源代碼中的崩潰行,而且只要編譯器可以生成 MAP 文件(包括 VC、MASM、VB、BCB、Delphi……),本方法都是適用的。我們時(shí)常抱怨 M$ 的產(chǎn)品如何如何差,但其實(shí) M$ 還是有意無(wú)意間提供了很多有價(jià)值的信息給我們的,只是我們往往不懂得怎么利用而已……相信這樣一來(lái),你就可以更為從容地面對(duì)“非法操作”提示了。你甚至可以要求用戶提供崩潰的地址,然后就可以坐在家中舒舒服服地找到出錯(cuò)的那行,并進(jìn)行修正。
是不是很爽呢? :)
對(duì)“僅通過(guò)崩潰地址找出源代碼的出錯(cuò)行”一文的補(bǔ)充與改進(jìn)
作者:上海偉功通信 roc
下載源代碼
讀了老羅的“僅通過(guò)崩潰地址找出源代碼的出錯(cuò)行”(下稱"羅文")一文后,感覺(jué)該文還是可以學(xué)到不少東西的。不過(guò)文中尚存在有些說(shuō)法不妥,以及有些操作太繁瑣的地方 。為此,本人在學(xué)習(xí)了此文后,在多次實(shí)驗(yàn)實(shí)踐基礎(chǔ)上,把該文中的一些內(nèi)容進(jìn)行補(bǔ)充與改進(jìn),希望對(duì)大家調(diào)試程序,尤其是release版本的程序有幫助 。歡迎各位朋友批評(píng)指正。
一、該方法適用的范圍
在windows程序中造成程序崩潰的原因很多,而文中所述的方法僅適用與:由一條語(yǔ)句當(dāng)即引起的程序崩潰。如原文中舉的除數(shù)為零的崩潰例子。而筆者在實(shí)際工作中碰到更多的情況是:指針指向一非法地址 ,然后對(duì)指針的內(nèi)容進(jìn)行了,讀或?qū)懙牟僮?。例如?/p>
void Crash1()
{
char * p =(char*)100;
*p=100;
} 這些原因造成的崩潰,無(wú)論是debug版本,還是release版本的程序,使用該方法都可找到造成崩潰的函數(shù)或子程序中的語(yǔ)句行,具體方法的下面還會(huì)補(bǔ)充說(shuō)明。 另外,實(shí)踐中另一種常見的造成程序崩潰的原因:函數(shù)或子程序中局部變量數(shù)組越界付值,造成函數(shù)或子程序返回地址遭覆蓋,從而造成函數(shù)或子程序返回時(shí)崩潰。例如:
#include
void Crash2();
int main(int argc,char* argv[])
{
Crash2();
return 0;
}
void Crash2()
{
char p[1];
strcpy(p,"0123456789");
}在vc中編譯運(yùn)行此程序的release版本,會(huì)跳出如下的出錯(cuò)提示框。
圖一 上面例子運(yùn)行結(jié)果
這里顯示的崩潰地址為:0x34333231。這種由前面語(yǔ)句造成的崩潰根源,在后續(xù)程序中方才顯現(xiàn)出來(lái)的情況,顯然用該文所述的方法就無(wú)能為力了。不過(guò)在此例中多少還有些蛛絲馬跡可尋找到崩潰的原因:函數(shù)Crash2中的局部數(shù)組p只有一個(gè)字節(jié)大小 ,顯然拷貝"0123456789"這個(gè)字符串會(huì)把超出長(zhǎng)度的字符串拷貝到數(shù)組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型數(shù)據(jù)是低字節(jié)保存在低地址中 ,所以保存字符串''1234''的內(nèi)存,顯示為一個(gè)4字節(jié)的int型數(shù)時(shí)就是0x34333231。顯然拷貝"0123456789"這個(gè)字符串時(shí),"1234"這幾個(gè)字符把函數(shù)Crash2的返回地址給覆蓋 ,從而造成程序崩潰。對(duì)于類似的這種造成程序崩潰的錯(cuò)誤朋友們還有其他方法排錯(cuò)的話,歡迎一起交流討論。
二、設(shè)置編譯產(chǎn)生map文件的方法
該文中產(chǎn)生map文件的方法是手工添加編譯參數(shù)來(lái)產(chǎn)生map文件。其實(shí)在vc6的IDE中有產(chǎn)生map文件的配置選項(xiàng)的。操作如下:先點(diǎn)擊菜單"Project"->"Settings。。。",彈出的屬性頁(yè)中選中"Link"頁(yè) ,確保在"category"中選中"General",最后選中"Generate mapfile"的可選項(xiàng)。若要在在map文件中顯示Line numbers的信息的話 ,還需在project options 中加入/mapinfo:lines 。Line numbers信息對(duì)于"羅文"所用的方法來(lái)定位出錯(cuò)源代碼行很重要 ,但筆者后面會(huì)介紹更加好的方法來(lái)定位出錯(cuò)代碼行,那種方法不需要Line numbers信息。
圖二 設(shè)置產(chǎn)生MAP文件
三、定位崩潰語(yǔ)句位置的方法
"羅文"所述的定位方法中,找到產(chǎn)生崩潰的函數(shù)位置的方法是正確的,即在map文件列出的每個(gè)函數(shù)的起始地址中,最近的且不大于崩潰地址的地址即為包含崩潰語(yǔ)句的函數(shù)的地址 。但之后的再進(jìn)一步的定位出錯(cuò)語(yǔ)句行的方法不是最妥當(dāng),因?yàn)槟欠N方法前提是,假設(shè)基地址的值是 0x00400000 ,以及一般的 PE 文件的代碼段都是從 0x1000偏移開始的 。雖然這種情況很普遍,但在vc中還是可以基地址設(shè)置為其他數(shù),比如設(shè)置為0x00500000,這時(shí)仍舊套用
崩潰行偏移 = 崩潰地址 - 0x00400000 - 0x1000 的公式顯然無(wú)法找到崩潰行偏移。 其實(shí)上述公式若改為
崩潰行偏移 = 崩潰地址 - 崩潰函數(shù)絕對(duì)地址 + 函數(shù)相對(duì)偏移即可通用了。仍以"羅文"中的例子為例:"羅文"中提到的在其崩潰程序的對(duì)應(yīng)map文件中,崩潰函數(shù)的編譯結(jié)果為
0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo。obj 對(duì)與上述結(jié)果,在使用我的公式時(shí) ,"崩潰函數(shù)絕對(duì)地址"指00401020, 函數(shù)相對(duì)偏移指 00000020, 當(dāng)崩潰地址= 0x0040104a時(shí), 則 崩潰行偏移 = 崩潰地址 - 崩潰函數(shù)起始地址+ 函數(shù)相對(duì)偏移 = 0x0040104a - 0x00401020 + 0x00000020= 0x4a,結(jié)果與"羅文"計(jì)算結(jié)果相同 。但這個(gè)公式更通用。
四、更好的定位崩潰語(yǔ)句位置的方法。
其實(shí)除了依靠map文件中的Line numbers信息最終定位出錯(cuò)語(yǔ)句行外,在vc6中我們還可以通過(guò)編譯程序產(chǎn)生的對(duì)應(yīng)的匯編語(yǔ)句,二進(jìn)制碼,以及對(duì)應(yīng)c/c++語(yǔ)句為一體的"cod"文件來(lái)定位出錯(cuò)語(yǔ)句行 。先介紹一下產(chǎn)生這種包含了三種信息的"cod"文件的設(shè)置方法:先點(diǎn)擊菜單"Project"->"Settings。。。",彈出的屬性頁(yè)中選中"C/C++"頁(yè) ,然后在"Category"中選則"Listing Files",再在"Listing file type"的組合框中選擇"Assembly,Machine code, and source"。接下去再通過(guò)一個(gè)具體的例子來(lái)說(shuō)明這種方法的具體操作。
圖三 設(shè)置產(chǎn)生"cod"文件
準(zhǔn)備步驟1)產(chǎn)生崩潰的程序如下:
01 //****************************************************************
02 //文件名稱:crash。cpp
03 //作用: 演示通過(guò)崩潰地址找出源代碼的出錯(cuò)行新方法
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 }
準(zhǔn)備步驟2)按本文所述設(shè)置產(chǎn)生map文件(不需要產(chǎn)生Line numbers信息)。
準(zhǔn)備步驟3)按本文所述設(shè)置產(chǎn)生cod文件。
準(zhǔn)備步驟4)編譯。這里以debug版本為例(若是release版本需要將編譯選項(xiàng)改為不進(jìn)行任何優(yōu)化的選項(xiàng),否則上述代碼會(huì)因?yàn)閮?yōu)化時(shí)看作廢代碼而不被編譯,從而看不到崩潰的結(jié)果),編譯后產(chǎn)生一個(gè)"exe"文件 ,一個(gè)"map"文件,一個(gè)"cod"文件。
運(yùn)行此程序,產(chǎn)生如下如下崩潰提示:
圖四 上面例子運(yùn)行結(jié)果
排錯(cuò)步驟1)定位崩潰函數(shù)??梢圆樵僲ap文件獲得。我的機(jī)器編譯產(chǎn)生的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
...
對(duì)于崩潰地址0x00401082而言,小于此地址中最接近的地址(Rva+Base中的地址)為00401060,其對(duì)應(yīng)的函數(shù)名為?Crash1@@YAXXZ,由于所有以問(wèn)號(hào)開頭的函數(shù)名稱都是 C++ 修飾的名稱 ,"@@YAXXZ"則為區(qū)別重載函數(shù)而加的后綴,所以?Crash1@@YAXXZ就是我們的源程序中,Crash1() 這個(gè)函數(shù)。
排錯(cuò)步驟2)定位出錯(cuò)行。打開編譯生成的"cod"文件,我機(jī)器上生成的文件內(nèi)容如下:
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匯編代碼的起始行。產(chǎn)生崩潰的代碼便在其后的某個(gè)位置。接下去的一行為:
; 15 : {冒號(hào)后的"{"表示源文件中的語(yǔ)句,冒號(hào)前的"15"表示該語(yǔ)句在源文件中的行數(shù)。 這之后顯示該語(yǔ)句匯編后的偏移地址,二進(jìn)制碼,匯編代碼。如
00000 55 push ebp其中"0000"表示相對(duì)于函數(shù)開始地址后的偏移,"55"為編譯后的機(jī)器代碼," push ebp"為匯編代碼。從"cod"文件中我們可以看出,一條(c/c++)語(yǔ)句通常需要編譯成數(shù)條匯編語(yǔ)句 。此外有些匯編語(yǔ)句太長(zhǎng)則會(huì)分兩行顯示如:
00018 c7 45 fc 64 00
00 00 mov DWORD PTR _p$[ebp], 100 ; 00000064H其中"0018"表示相對(duì)偏移,在debug版本中,這個(gè)數(shù)據(jù)為相對(duì)于函數(shù)起始地址的偏移(此時(shí)每個(gè)函數(shù)第一條語(yǔ)句相對(duì)偏移為0000);release版本中為相對(duì)于代碼段第一條語(yǔ)句的偏移(即代碼段第一條語(yǔ)句相對(duì)偏移為0000,而以后的每個(gè)函數(shù)第一條語(yǔ)句相對(duì)偏移就不為0000了)。"c7 45 fc 64 00 00 00 "為編譯后的機(jī)器代碼 ,"mov DWORD PTR _p$[ebp], 100"為匯編代碼, 匯編語(yǔ)言中";"后的內(nèi)容為注釋,所以";00000064H",是個(gè)注釋這里用來(lái)說(shuō)明100轉(zhuǎn)換成16進(jìn)制時(shí)為"00000064H"。
接下去,我們開始來(lái)定位產(chǎn)生崩潰的語(yǔ)句。
第一步,計(jì)算崩潰地址相對(duì)于崩潰函數(shù)的偏移,在本例中已經(jīng)知道了崩潰語(yǔ)句的地址(0x00401082),和對(duì)應(yīng)函數(shù)的起始地址(0x00401060),所以崩潰地址相對(duì)函數(shù)起始地址的偏移就很容易計(jì)算了:
崩潰偏移地址 = 崩潰語(yǔ)句地址 - 崩潰函數(shù)的起始地址 = 0x00401082 - 0x00401060 = 0x22。第二步,計(jì)算出錯(cuò)的匯編語(yǔ)句在cod文件中的相對(duì)偏移。我們可以看到函數(shù)Crash1()在cod文件中的相對(duì)偏移地址為0000,則
崩潰語(yǔ)句在cod文件中的相對(duì)偏移 = 崩潰函數(shù)在cod文件中相對(duì)偏移 + 崩潰偏移地址 = 0x0000 + 0x22 = 0x22第三步,我們看Crash1函數(shù)偏移0x22除的代碼是什么?結(jié)果如下
00022 c6 00 64 mov BYTE PTR [eax], 100 ; 00000064H這句匯編語(yǔ)句表示將100這個(gè)數(shù)保存到寄存器eax所指的內(nèi)存單元中去,保存空間大小為1個(gè)字節(jié)(byte)。程序正是執(zhí)行這條命令時(shí)產(chǎn)生了崩潰,顯然這里eax中的為一個(gè)非法地址 ,所以程序崩潰了!
第四步,再查看該匯編語(yǔ)句在其前面幾行的其對(duì)應(yīng)的源代碼,結(jié)果如下:
; 17 : *p=100;其中17表示該語(yǔ)句位于源文件中第17行,而“*p=100;”這正是源文件中產(chǎn)生崩潰的語(yǔ)句。
至此我們僅從崩潰地址就查找出了造成崩潰的源代碼語(yǔ)句和該語(yǔ)句所在源文件中的確切位置,甚至查找到了造成崩潰的編譯后的確切匯編代碼!
怎么樣,是不是感覺(jué)更爽啊?
五、小節(jié)
1、新方法同樣要注意可以適用的范圍,即程序由一條語(yǔ)句當(dāng)即引起的崩潰。另外我不知道除了VC6外,是否還有其他的編譯器能夠產(chǎn)生類似的"cod"文件。
2、我們可以通過(guò)比較 新方法產(chǎn)生的debug和releae版本的"cod"文件,查找那些僅release版本(或debug版本)有另一個(gè)版本沒(méi)有的bug(或其他性狀)。例如"羅文"中所舉的那個(gè)用例 ,只要打開release版本的"cod"文件,就明白了為啥debug版本會(huì)產(chǎn)生崩潰而release版本卻沒(méi)有:原來(lái)release版本中產(chǎn)生崩潰的語(yǔ)句其實(shí)根本都沒(méi)有編譯 。同樣本例中的release版本要看到崩潰的效果,需要將編譯選項(xiàng)改為為不優(yōu)化的配置。
關(guān)于MFC下檢查和消除內(nèi)存泄露的技巧
作者:freepublic
摘要
本文分析了Windows環(huán)境使用MFC調(diào)試內(nèi)存泄露的技術(shù),介紹了在Windows環(huán)境下用VC++查找,定位和消除內(nèi)存泄露的方法技巧。
關(guān)鍵詞:VC++;CRT 調(diào)試堆函數(shù);試探法。
編譯環(huán)境
VC++6.0
技術(shù)原理
檢測(cè)內(nèi)存泄漏的主要工具是調(diào)試器和 CRT 調(diào)試堆函數(shù)。若要啟用調(diào)試堆函數(shù),請(qǐng)?jiān)诔绦蛑邪ㄒ韵抡Z(yǔ)句:
#define CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>注意 #include 語(yǔ)句必須采用上文所示順序。如果更改了順序,所使用的函數(shù)可能無(wú)法正確工作。
通過(guò)包括 crtdbg.h,將 malloc 和 free 函數(shù)映射到其“Debug”版本_malloc_dbg 和_free_dbg,這些函數(shù)將跟蹤內(nèi)存分配和釋放。此映射只在調(diào)試版本(在其中定義了 _DEBUG)中發(fā)生。發(fā)布版本使用普通的 malloc 和 free 函數(shù)。
#define 語(yǔ)句將 CRT 堆函數(shù)的基版本映射到對(duì)應(yīng)的“Debug”版本。并非絕對(duì)需要該語(yǔ)句,但如果沒(méi)有該語(yǔ)句,內(nèi)存泄漏轉(zhuǎn)儲(chǔ)包含的有用信息將較少。
在添加了上面所示語(yǔ)句之后,可以通過(guò)在程序中包括以下語(yǔ)句來(lái)轉(zhuǎn)儲(chǔ)內(nèi)存泄漏信息:
_CrtDumpMemoryLeaks();當(dāng)在調(diào)試器下運(yùn)行程序時(shí),_CrtDumpMemoryLeaks 將在“輸出”窗口中顯示內(nèi)存泄漏信息。內(nèi)存泄漏信息如下所示:
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 語(yǔ)句,內(nèi)存泄漏轉(zhuǎn)儲(chǔ)如下所示:
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 時(shí),所顯示的會(huì)是:
內(nèi)存分配編號(hào)(在大括號(hào)內(nèi))。
塊類型(普通、客戶端或 CRT)。
十六進(jìn)制形式的內(nèi)存位置。
以字節(jié)為單位的塊大小。
前 16 字節(jié)的內(nèi)容(亦為十六進(jìn)制)。
定義了 _CRTDBG_MAP_ALLOC 時(shí),還會(huì)顯示在其中分配泄漏的內(nèi)存的文件。文件名后括號(hào)中的數(shù)字(本示例中為 20)是該文件內(nèi)的行號(hào)。
轉(zhuǎn)到源文件中分配內(nèi)存的行
在"輸出"窗口中雙擊包含文件名和行號(hào)的行。
-或-
在"輸出"窗口中選擇包含文件名和行號(hào)的行,然后按 F4 鍵。
_CrtSetDbgFlag 如果程序總在同一位置退出,則調(diào)用 _CrtDumpMemoryLeaks 足夠方便,但如果程序可以從多個(gè)位置退出該怎么辦呢?不要在每個(gè)可能的出口放置一個(gè)對(duì) _CrtDumpMemoryLeaks 的調(diào)用,可以在程序開始包括以下調(diào)用:
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); 該語(yǔ)句在程序退出時(shí)自動(dòng)調(diào)用 _CrtDumpMemoryLeaks。必須同時(shí)設(shè)置 _CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF 兩個(gè)位域,如上所示。
說(shuō)明
在VC++6.0的環(huán)境下,不再需要額外的添加
#define CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h> 只需要按F5,在調(diào)試狀態(tài)下運(yùn)行,程序退出后在"輸出窗口"可以看到有無(wú)內(nèi)存泄露。如果出現(xiàn)
Detected memory leaks!
Dumping objects -> 就有內(nèi)存泄露。
確定內(nèi)存泄露的地方
根據(jù)內(nèi)存泄露的報(bào)告,有兩種消除的方法:
第一種比較簡(jiǎn)單,就是已經(jīng)把內(nèi)存泄露映射到源文件的,可以直接在"輸出"窗口中雙擊包含文件名和行號(hào)的行。例如
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)就是源文件名稱和行號(hào)。
第二種比較麻煩,就是不能映射到源文件的,只有內(nèi)存分配塊號(hào)。
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. 這種情況我采用一種"試探法"。由于內(nèi)存分配的塊號(hào)不是固定不變的,而是每次運(yùn)行都是變化的,所以跟蹤起來(lái)很麻煩。但是我發(fā)現(xiàn)雖然內(nèi)存分配的塊號(hào)是變化的,但是變化的塊號(hào)卻總是那幾個(gè),也就是說(shuō)多運(yùn)行幾次,內(nèi)存分配的塊號(hào)很可能會(huì)重復(fù)。因此這就是"試探法"的基礎(chǔ)。
先在調(diào)試狀態(tài)下運(yùn)行幾次程序,觀察內(nèi)存分配的塊號(hào)是哪幾個(gè)值;
選擇出現(xiàn)次數(shù)最多的塊號(hào)來(lái)設(shè)斷點(diǎn),在代碼中設(shè)置內(nèi)存分配斷點(diǎn): 添加如下一行(對(duì)于第 18 個(gè)內(nèi)存分配):
_crtBreakAlloc = 18; 或者,可以使用具有同樣效果的 _CrtSetBreakAlloc 函數(shù):
_CrtSetBreakAlloc(18);
在調(diào)試狀態(tài)下運(yùn)行序,在斷點(diǎn)停下時(shí),打開"調(diào)用堆棧"窗口,找到對(duì)應(yīng)的源代碼處;
退出程序,觀察"輸出窗口"的內(nèi)存泄露報(bào)告,看實(shí)際內(nèi)存分配的塊號(hào)是不是和預(yù)設(shè)值相同,如果相同,就找到了;如果不同,就重復(fù)步驟3,直到相同。
最后就是根據(jù)具體情況,在適當(dāng)?shù)奈恢冕尫潘峙涞膬?nèi)存。
發(fā)表于 @ 2007年05月17日 15:56:00 | 評(píng)論( 0 ) | 編輯| 舉報(bào)| 收藏
舊一篇:VC++6.0中內(nèi)存泄漏檢測(cè)(轉(zhuǎn)) | 新一篇:_CrtDumpMemoryLeaks()的作用(轉(zhuǎn))
查看最新精華文章 請(qǐng)?jiān)L問(wèn)博客首頁(yè)相關(guān)文章
vc++內(nèi)存泄漏檢測(cè)使用 CRT 調(diào)試功能來(lái)檢測(cè)內(nèi)存泄漏VC使用CRT調(diào)試功能來(lái)檢測(cè)內(nèi)存泄漏檢測(cè)內(nèi)存泄漏使用CRT調(diào)試功能來(lái)檢測(cè)內(nèi)存泄漏VC使用CRT調(diào)試功能來(lái)檢測(cè)內(nèi)存泄漏發(fā)表評(píng)論 表 情: 評(píng)論內(nèi)容: 用 戶 名: 登錄 注冊(cè) 匿名評(píng)論 匿名用戶驗(yàn) 證 碼: 重新獲得驗(yàn)證碼 熱門招聘職位【《七雄爭(zhēng)霸》研發(fā)商:北京游戲谷】誠(chéng)聘JAVA、C++技術(shù)專家及各類游戲精英【華北計(jì)算技術(shù)研究所】急聘Java開發(fā)、系統(tǒng)架構(gòu)、需求分析【無(wú)錫富贏科技有限公司】誠(chéng)聘 程序員ASP.NET及軟件開發(fā)工程師C++【勵(lì)展博覽集團(tuán)】急聘IT Application Developer企業(yè)應(yīng)用程序開發(fā)員【西安瑞祺科技】誠(chéng)聘JAVA軟件工程師【雅邦網(wǎng)絡(luò)】誠(chéng)聘網(wǎng)站項(xiàng)目經(jīng)理、JAVA技術(shù)總監(jiān)、JAVA開發(fā)、網(wǎng)站測(cè)試工程師【科銳】誠(chéng)招IT中高級(jí)人才,搶工作機(jī)會(huì),贏IPAD啦!【imo】-國(guó)際風(fēng)投+福利租房+獎(jiǎng)金+期權(quán)+不加班+調(diào)休,邀你共創(chuàng)互聯(lián)網(wǎng)的奇跡【YOHO!新力傳媒】高薪誠(chéng)聘各類網(wǎng)站人才 北京+南京【方正國(guó)際】誠(chéng)招軟件精英 北京+蘇州+武漢【熱聘】搜狐暢游全國(guó)熱招開發(fā)工程師【愛立信上?!考闭卸嗝襟w、核心網(wǎng)開發(fā)測(cè)試工程師,國(guó)際團(tuán)隊(duì)等你加盟! 【《七雄爭(zhēng)霸》研發(fā)商:北京游戲谷】誠(chéng)聘JAVA、C++技術(shù)專家及各類游戲精英【華北計(jì)算技術(shù)研究所】急聘Java開發(fā)、系統(tǒng)架構(gòu)、需求分析【無(wú)錫富贏科技有限公司】誠(chéng)聘 程序員ASP.NET及軟件開發(fā)工程師C++【勵(lì)展博覽集團(tuán)】急聘IT Application Developer企業(yè)應(yīng)用程序開發(fā)員【西安瑞祺科技】誠(chéng)聘JAVA軟件工程師【雅邦網(wǎng)絡(luò)】誠(chéng)聘網(wǎng)站項(xiàng)目經(jīng)理、JAVA技術(shù)總監(jiān)、JAVA開發(fā)、網(wǎng)站測(cè)試工程師【科銳】誠(chéng)招IT中高級(jí)人才,搶工作機(jī)會(huì),贏IPAD啦!【imo】-國(guó)際風(fēng)投+福利租房+獎(jiǎng)金+期權(quán)+不加班+調(diào)休,邀你共創(chuàng)互聯(lián)網(wǎng)的奇跡【YOHO!新力傳媒】高薪誠(chéng)聘各類網(wǎng)站人才 北京+南京【方正國(guó)際】誠(chéng)招軟件精英 北京+蘇州+武漢【熱聘】搜狐暢游全國(guó)熱招開發(fā)工程師【愛立信上?!考闭卸嗝襟w、核心網(wǎng)開發(fā)測(cè)試工程師,國(guó)際團(tuán)隊(duì)等你加盟! 公司簡(jiǎn)介|招賢納士|廣告服務(wù)|銀行匯款賬號(hào)|聯(lián)系方式|版權(quán)聲明|法律顧問(wèn)|問(wèn)題報(bào)告
北京創(chuàng)新樂(lè)知信息技術(shù)有限公司 版權(quán)所有, 京 ICP 證 070598 號(hào)
世紀(jì)樂(lè)知(北京)網(wǎng)絡(luò)技術(shù)有限公司 提供技術(shù)支持
江蘇樂(lè)知網(wǎng)絡(luò)技術(shù)有限公司 提供商務(wù)支持
Email:webmaster@csdn.net
Copyright © 1999-2010, CSDN.NET, All Rights Reserved
本文來(lái)自CSDN博客,轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/bairny/archive/2007/05/17/1613431.aspx
聯(lián)系客服