VC調試入門 c++
做者:阿榮程序員
概述
調試是一個程序員最基本的技能,其重要性甚至超過學習一門語言。不會調試的程序員就意味着他即便會一門語言,卻不能編制出任何好的軟件。
這裏我簡要的根據本身的經驗列出調試中比較經常使用的技巧,但願對你們有用。
本文約定,在選擇菜單時,經過/表示分級菜單,例如File/Open表示頂級菜單File的子菜單Open。
設置
爲了調試一個程序,首先必須使程序中包含調試信息。通常狀況下,一個從AppWizard建立的工程中包含的Debug Configuration自動包含調試信息,可是是否是Debug版本並非程序包含調試信息的決定因素,程序設計者能夠在任意的Configuration中增長調試信息,包括Release版本。
爲了增長調試信息,能夠按照下述步驟進行:windows
命令行 | Project settings | 說明 |
無 | None | 沒有調試信息 |
/Zd | Line Numbers Only | 目標文件或者可執行文件中只包含全局和導出符號以及代碼行信息,不包含符號調試信息 |
/Z7 | C 7.0- Compatible | 目標文件或者可執行文件中包含行號和全部符號調試信息,包括變量名及類型,函數及原型等 |
/Zi | Program Database | 建立一個程序庫(PDB),包括類型信息和符號調試信息。 |
/ZI | Program Database for Edit and Continue | 除了前面/Zi的功能外,這個選項容許對代碼進行調試過程當中的修改和繼續執行。這個選項同時使#pragma設置的優化功能無效 |
斷點
斷點是調試器設置的一個代碼位置。當程序運行到斷點時,程序中斷執行,回到調試器。斷點是 最經常使用的技巧。調試時,只有設置了斷點並使程序回到調試器,才能對程序進行在線調試。
設置斷點:能夠經過下述方法設置一個斷點。首先把光標移動到須要設置斷點的代碼行上,而後api
去掉斷點:把光標移動到給定斷點所在的行,再次按F9就能夠取消斷點。同前面所述,打開Breakpoints對話框後,也能夠按照界面提示去掉斷點。
條件斷點:能夠爲斷點設置一個條件,這樣的斷點稱爲條件斷點。對於新加的斷點,能夠單擊Conditions按鈕,爲斷點設置一個表達式。當這個表達式發生改變時,程序就 被中斷。底下設置包括「觀察數組或者結構的元素個數」,彷佛能夠設置一個指針所指向的內存區的大小,可是我設置一個比較的值可是改動 範圍以外的內存區彷佛也致使斷點起效。最後一個設置可讓程序先執行多少次而後纔到達斷點。
數據斷點:數據斷點只能在Breakpoints對話框中設置。選擇「Data」頁,就顯示了設置數據斷點的對話框。在編輯框中輸入一個表達式,當這個 表達式的值發生變化時,數據斷點就到達。通常狀況下,這個表達式應該由運算符和全局變量構成,例如:在編輯框中輸入 g_bFlag這個全局變量的名字,那麼當程序中有g_bFlag= !g_bFlag時,程序就將停在這個語句處。
消息斷點:VC也支持對Windows消息進行截獲。他有兩種方式進行截獲:窗口消息處理函數和特定消息中斷。
在Breakpoints對話框中選擇Messages頁,就能夠設置消息斷點。若是在上面那個對話框中寫入消息處理函數的名字,那麼 每次消息被這個函數處理,斷點就到達(我以爲若是採用普通斷點在這個函數中截獲,效果應該同樣)。若是在底下的下拉 列表框選擇一個消息,則每次這種消息到達,程序就中斷。
值
Watch
VC支持查看變量、表達式和內存的值。全部這些觀察都必須是在斷點中斷的狀況下進行。
觀看變量的值最簡單,當斷點到達時,把光標移動到這個變量上,停留一會就能夠看到變量的值。
VC提供一種被成爲Watch的機制來觀看變量和表達式的值。在斷點狀態下,在變量上單擊右鍵,選擇Quick Watch, 就彈出一個對話框,顯示這個變量的值。
單擊Debug工具條上的Watch按鈕,就出現一個Watch視圖(Watch1,Watch2,Watch3,Watch4),在該視圖中輸入變量或者表達式,就能夠觀察 變量或者表達式的值。注意:這個表達式不能有反作用,例如++運算符絕對禁止用於這個表達式中,由於這個運算符將修改變量的值,致使 軟件的邏輯被破壞。
Memory
因爲指針指向的數組,Watch只能顯示第一個元素的值。爲了顯示數組的後續內容,或者要顯示一片內存的內容,可使用memory功能。在 Debug工具條上點memory按鈕,就彈出一個對話框,在其中輸入地址,就能夠顯示該地址指向的內存的內容。
Varibles
Debug工具條上的Varibles按鈕彈出一個框,顯示全部當前執行上下文中可見的變量的值。特別是當前指令涉及的變量,以紅色顯示。
寄存器
Debug工具條上的Reigsters按鈕彈出一個框,顯示當前的全部寄存器的值。
進程控制
VC容許被中斷的程序繼續運行、單步運行和運行到指定光標處,分別對應快捷鍵F五、F10/F11和CTRL+F10。各個快捷鍵功能以下:
數組
快捷鍵 | 說明 |
F5 | 繼續運行 |
F10 | 單步,若是涉及到子函數,不進入子函數內部 |
F11 | 單步,若是涉及到子函數,進入子函數內部 |
CTRL+F10 | 運行到當前光標處。 |
Call Stack
調用堆棧反映了當前斷點處函數是被那些函數按照什麼順序調用的。單擊Debug工具條上的Call stack就顯示Call Stack對話框。在CallStack對話框中顯示了一個調用系列,最上面的是當前函數,往下依次是調用函數的上級函數。單擊這些函數名能夠跳到對應的函數中去。
其餘調試手段
系統提供一系列特殊的函數或者宏來處理Debug版本相關的信息,以下:數據結構
宏名/函數名 | 說明 |
TRACE | 使用方法和printf徹底一致,他在output框中輸出調試信息 |
ASSERT | 它接收一個表達式,若是這個表達式爲TRUE,則無動做,不然中斷當前程序執行。對於系統中出現這個宏 致使的中斷,應該認爲你的函數調用未能知足系統的調用此函數的前提條件。例如,對於一個尚未建立的窗口調用SetWindowText等。 |
VERIFY | 和ASSERT功能相似,所不一樣的是,在Release版本中,ASSERT不計算輸入的表達式的值,而VERIFY計算表達式的值。 |
關注
一個好的程序員不該該把全部的判斷交給編譯器和調試器,應該在程序中本身加以程序保護和錯誤定位,具體措施包括:多線程
另外:VC中要編制程序不該該一開始就寫cpp/h文件,而應該首先建立一個合適的工程。由於只有這樣,VC才能選擇合適的編譯、鏈接 選項。對於加入到工程中的cpp文件,應該檢查是否在第一行顯式的包含stdafx.h頭文件,這是Microsoft Visual Studio爲了加快編譯 速度而設置的預編譯頭文件。在這個#include "stdafx.h"行前面的全部代碼將被忽略,因此其餘頭文件應該在這一行後面被包含。
對於.c文件,因爲不能包含stdafx.h,所以能夠經過Project settings把它的預編譯頭設置爲「不使用」,方法是:ide
關於調試時輸出的字符串信息
做者:①塌糊塗函數
下載源代碼
使用工具: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版下運行程序沒有輸出調試信息來,可這些調試信息卻留在了二進制的可執行文件裏;
咱們能夠用IDA來打開該Release版的可執行文件,看到如圖一所示的信息:
圖一:IDA反彙編後的main函數
注:該函數就是main函數
可見調試信息字符串(「Some debug information here!\n」)確實存在於Release版的可執行文件裏;
咱們固然不但願別人看到這些調試信息,那有沒有辦法來防止該調試信息被編譯進Release版的可執行文件裏呢?
辦法是有的,這裏來描述2個方法。
辦法一:
定義以下宏:
#if _DEBUG #define _D(str) str #else #define _D(str) NULL #endif
此時輸出語句變爲:
printInfo(_D("Some debug information here!\n"));
在Debug模式下運行程序,依然輸出調試信息:
「Some debug information here!」;
在Release下,則什麼都不輸出,此時咱們用IDA看一下Release版的二進制文件,則沒有發現該調試信息字符串。
如圖二示:
圖二:IDA反彙編後的main函數
方法二:
定義以下宏:
#if _DEBUG void printInfo(char *strInfo) { printf(strInfo); } #else #define printInfo(str) #endif
注意:該宏把函數printInfo的定義也放進去了;
在Debug模式下運行程序,也一樣輸出調試信息:
「Some debug information here!」;
在Release下,也什麼都不輸出,此時咱們用IDA看一下Release版的二進制文件,也沒有發現該調試信息字符串。
如圖三示:
圖三:IDA反彙編後的main函數
既然方法一和方法二都能實現一樣的功能,那究竟那個方法好呢?
方法一和方法二確實都沒在可執行文件裏留下調試信息,比較一下圖二和圖三,咱們不難發現:
圖二當中多了一個函數調用 call nullsub_1,該函數就是printInfo,雖然該函數什麼都不作,
但它卻調用了,咱們通常也不但願該函數調用,因此方法一中多了一個函數調用,增長了開銷,
而方法二當中卻沒有調用該函數。
我的認爲方法二較好。
結束語:
若要轉載該文章,請保持原文章的完整性,謝謝!
文中若有不妥之處,請指正,謝謝!
E-mail:grapeky@etang.com
調用規範與可變參數表
做者:阿半
語言調用規範是指進行一次函數調用所採用的傳遞參數的方法,返回值的處理以及調用堆棧的清理。Microsoft C/C++ 語言中採用了五種調用規範,分別是__cdecl, __stdcall, __fastcall,thiscall和nake每一中調用規範都是利用eax做爲返回值,若是函數返回值是64位的,則利用edx:eax對來返回值。Nake調用規範很是的靈活,足以獨立的一篇文章描述,這裏就再也不描述nake調用規範。下表列出了前面四種規範調用的特色:
關鍵字 | 堆棧清理者 | 參數傳遞順序 |
__cdecl | 調用者 | 從右至左 |
__stdcall | 被調用者 | 從右至左 |
__fastcall | 被調用者 | 從右至左,前兩個參數由寄存器ecx,edx傳遞 |
thiscall | 被調用者或者調用者 | 從右至左 |
__cdecl 最大好處在於因爲是調用者清理棧,它能夠處理可變參數,缺點則在於它增長了程序的大小,由於在每一個調用返回的時候,須要多執行一條清理棧的指令。
__stdcall 是在windows程序設計中出現的最多的調用規則,全部的不可變參數的API調用都使用這個規則。
__fastcall 在windows內核設計中被普遍的使用,因爲兩個參數由寄存器直接傳遞,採用這種規則的函數效率要比以上兩種規則高。
thiscall是C++成員函數的默認調用規範,編譯期間,這種調用會根據函數是否支持可變參數表來決定採用什麼方式清理堆棧。若是成員函數不支持可變參數,那麼它就是用參數入棧,ecx保存this指針的方式進行調用,若是成員函數支持可變參數,那麼它的調用和__cdecl相似,惟一不一樣的是將this指針最後壓入棧中進行傳遞。
調用者和被調用者必須採用一樣的規則才能保證程序的正常執行,曾經看到不少程序員犯的錯誤就是因爲調用規範的不同,導致程序異常,好比:
DWORD ThreadFunc(LPVOID lpParam) { //… } CreateThread(..,(LPTHREAD_START_ROUTINE)ThreadFunc, …);
若是在編譯期間沒有指定編譯選項/Gz(指定未指明調用規範的函數採用__stdcall方式),那麼編譯器自動將ThreadFunc處理成__cdecl調用規範(/Gd),這樣可能在線程開始的時候正常執行,然而退出的時候因爲堆棧沒有正常清理,形成訪問違例或者非法指令錯誤。
以上說了不少清理棧的問題,那麼爲何清理棧很重要呢。堆棧是線程相關的,也就是說每個線程含有一個堆棧,這個堆棧上保存了局部變量,調用返回地址等不少線程相關的數據,這也是爲何獨立運行的線程能夠調用一樣一個函數而互不干擾的緣由。堆棧的特色恐怕你們已經很是熟悉了,那麼根據上面的每一種調用,我給出一個簡單的圖示來講明清理堆棧的重要性,以及爲何上面的例子代碼會出錯。
圖一 這是線程堆棧在運行的時候的樣子
調用前和後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調用,而實際的線程函數被編譯後,並無執行堆棧的清理工做,線程函數返回的時候,因爲堆棧的不正確,固然會發生錯誤。修改這個bug的方法只要在線程函數的定義前把__cdecl改爲_stdcall便可。
有了上面的例子作基礎來理解可變參數表就簡單的多了,因爲各類調用規範的限定,導致只有__cdecl調用規範能夠採用可變參數表。先來看看可變參數表的定義(能夠參考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()就是操做系統可訪問的地址空間的大小,也就是CPU相關的字長。_INTSIZEOF宏很簡單,就是用來將數據以n的數據大小對齊。va_start宏有點模糊,但是若是你看懂了上面的堆棧數據結構,那麼顯然它就是得到最後一個固定參數的地址,也就是堆棧上的地址,va_arg先使得ap指向下一個參數,而後取得當前參數的值(注意,這個值正是堆棧上的值),va_end使得取參數過程結束。
這幾個宏完成的動做很簡單了,實際就是取得可變參數表在堆棧上的起始位置,而後根據參數類型,依次從堆棧上取出每個參數。
本文簡單的介紹了微軟C/C++支持的調用類型,結合實例描述了規範的實際應用,最後根據CRT提供的源代碼分析了可變參數表的實現。
僅經過崩潰地址找出源代碼的出錯行
做者:老羅
提交者:eastvc 發佈日期:2003-10-23 9:16:11
原文出處:http://www.luocong.com/articles/show_article.asp?Article_ID=29
做爲程序員,咱們平時最擔憂見到的事情是什麼?是內存泄漏?是界面很差看?……錯啦!我相信個人見解是不會有人反對的——那就是,程序發生了崩潰!
「該程序執行了非法操做,即將關閉。請與你的軟件供應商聯繫。」,呵呵,這句 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 可執行文件和 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 可執行文件和 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 這欄,你會發現第一個比崩潰地址 0x0040104a 大的函數地址是 0x00401070 ,因此在 0x00401070 這個地址以前的那個入口就是產生崩潰的函數,也就是這行:
0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo.obj
所以,發生崩潰的函數就是 ?Crash@@YAXXZ ,全部以問號開頭的函數名稱都是 C++ 修飾的名稱。在咱們的源程序中,也就是 Crash() 這個子函數。
OK,如今咱們垂手可得地便知道了發生崩潰的函數名稱,你是否是很興奮呢?呵呵,先別忙,接下來,更厲害的招數要出場了。
請注意 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版本的程序有幫助 。歡迎各位朋友批評指正。
1、該方法適用的範圍
在windows程序中形成程序崩潰的緣由不少,而文中所述的方法僅適用與:由一條語句立即引發的程序崩潰。如原文中舉的除數爲零的崩潰例子。而筆者在實際工做中碰到更多的狀況是:指針指向一非法地址 ,而後對指針的內容進行了,讀或寫的操做。例如:
void Crash1() { char * p =(char*)100; *p=100; }
這些緣由形成的崩潰,不管是debug版本,仍是release版本的程序,使用該方法均可找到形成崩潰的函數或子程序中的語句行,具體方法的下面還會補充說明。 另外,實踐中另外一種常見的形成程序崩潰的緣由:函數或子程序中局部變量數組越界付值,形成函數或子程序返回地址遭覆蓋,從而形成函數或子程序返回時崩潰。例如:
#includevoid Crash2(); int main(int argc,char* argv[]) { Crash2(); return 0; } void Crash2() { char p[1]; strcpy(p,"0123456789"); }
在vc中編譯運行此程序的release版本,會跳出以下的出錯提示框。
圖一 上面例子運行結果
這裏顯示的崩潰地址爲:0x34333231。這種由前面語句形成的崩潰根源,在後續程序中方纔顯現出來的狀況,顯然用該文所述的方法就無能爲力了。不過在此例中多少還有些蛛絲馬跡可尋找到崩潰的緣由:函數Crash2中的局部數組p只有一個字節大小 ,顯然拷貝"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型數據是低字節保存在低地址中 ,因此保存字符串''1234''的內存,顯示爲一個4字節的int型數時就是0x34333231。顯然拷貝"0123456789"這個字符串時,"1234"這幾個字符把函數Crash2的返回地址給覆蓋 ,從而形成程序崩潰。對於相似的這種形成程序崩潰的錯誤朋友們還有其餘方法排錯的話,歡迎一塊兒交流討論。
2、設置編譯產生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文件
3、定位崩潰語句位置的方法
"羅文"所述的定位方法中,找到產生崩潰的函數位置的方法是正確的,即在map文件列出的每一個函數的起始地址中,最近的且不大於崩潰地址的地址即爲包含崩潰語句的函數的地址 。但以後的再進一步的定位出錯語句行的方法不是最穩當,由於那種方法前提是,假設基地址的值是 0x00400000 ,以及通常的 PE 文件的代碼段都是從 0x1000偏移開始的 。雖然這種狀況很廣泛,但在vc中仍是能夠基地址設置爲其餘數,好比設置爲0x00500000,這時仍舊套用
崩潰行偏移 = 崩潰地址 - 0x00400000 - 0x1000
的公式顯然沒法找到崩潰行偏移。 其實上述公式若改成
崩潰行偏移 = 崩潰地址 - 崩潰函數絕對地址 + 函數相對偏移
便可通用了。仍以"羅文"中的例子爲例:"羅文"中提到的在其崩潰程序的對應map文件中,崩潰函數的編譯結果爲
0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo。obj
對與上述結果,在使用個人公式時 ,"崩潰函數絕對地址"指00401020, 函數相對偏移指 00000020, 當崩潰地址= 0x0040104a時, 則 崩潰行偏移 = 崩潰地址 - 崩潰函數起始地址+ 函數相對偏移 = 0x0040104a - 0x00401020 + 0x00000020= 0x4a,結果與"羅文"計算結果相同 。但這個公式更通用。
4、更好的定位崩潰語句位置的方法。
其實除了依靠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版本須要將編譯選項改成不進行任何優化的選項,不然上述代碼會由於優化時看做廢代碼而不被編譯,從而看不到崩潰的結果),編譯後產生一個"exe"文件 ,一個"map"文件,一個"cod"文件。
運行此程序,產生以下以下崩潰提示:
圖四 上面例子運行結果
排錯步驟1)定位崩潰函數。能夠查詢map文件得到。個人機器編譯產生的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"則爲區別重載函數而加的後綴,因此?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個字節(byte)。程序正是執行這條命令時產生了崩潰,顯然這裏eax中的爲一個非法地址 ,因此程序崩潰了!
第四步,再查看該彙編語句在其前面幾行的其對應的源代碼,結果以下:
; 17 : *p=100;
其中17表示該語句位於源文件中第17行,而「*p=100;」這正是源文件中產生崩潰的語句。
至此咱們僅從崩潰地址就查找出了形成崩潰的源代碼語句和該語句所在源文件中的確切位置,甚至查找到了形成崩潰的編譯後的確切彙編代碼!
怎麼樣,是否是感受更爽啊?
5、小節
一、新方法一樣要注意能夠適用的範圍,即程序由一條語句立即引發的崩潰。另外我不知道除了VC6外,是否還有其餘的編譯器可以產生相似的"cod"文件。
二、咱們能夠經過比較 新方法產生的debug和releae版本的"cod"文件,查找那些僅release版本(或debug版本)有另外一個版本沒有的bug(或其餘性狀)。例如"羅文"中所舉的那個用例 ,只要打開release版本的"cod"文件,就明白了爲啥debug版本會產生崩潰而release版本卻沒有:原來release版本中產生崩潰的語句其實根本都沒有編譯 。一樣本例中的release版本要看到崩潰的效果,須要將編譯選項改成爲不優化的配置。
關於MFC下檢查和消除內存泄露的技巧
做者:freepublic
摘要
本文分析了Windows環境使用MFC調試內存泄露的技術,介紹了在Windows環境下用VC++查找,定位和消除內存泄露的方法技巧。
關鍵詞:VC++;CRT 調試堆函數;試探法。
編譯環境
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)中發生。發佈版本使用普通的 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)。
十六進制形式的內存位置。
以字節爲單位的塊大小。
前 16 字節的內容(亦爲十六進制)。
定義了 _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的環境下,再也不須要額外的添加
#define CRTDBG_MAP_ALLOC #include <stdlib.h> #include <crtdbg.h>
只須要按F5,在調試狀態下運行,程序退出後在"輸出窗口"能夠看到有無內存泄露。若是出現
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.
這種狀況我採用一種"試探法"。因爲內存分配的塊號不是固定不變的,而是每次運行都是變化的,因此跟蹤起來很麻煩。可是我發現雖然內存分配的塊號是變化的,可是變化的塊號卻老是那幾個,也就是說多運行幾回,內存分配的塊號極可能會重複。所以這就是"試探法"的基礎。
_crtBreakAlloc = 18;或者,可使用具備一樣效果的 _CrtSetBreakAlloc 函數:
_CrtSetBreakAlloc(18);