VC++ 6.0 中如何使用 CRT 調試功能來檢測內存泄漏 - 2

 VC調試入門



作者:
阿榮


 概述
調試是一個程序員最基本的技能,其重要性甚至超過學習一門語言。不會調試的程序員就意味着他即使會一門語言,卻不能編制出任何好的軟件。
這裏我簡要的根據自己的經驗列出調試中比較常用的技巧,希望對大家有用。
本文約定,在選擇菜單時,通過/表示分級菜單,例如File/Open表示頂級菜單File的子菜單Open。
 
 設置
爲了調試一個程序,首先必須使程序中包含調試信息。一般情況下,一個從AppWizard創建的工程中包含的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 目標文件或者可執行文件中只包含全局和導出符號以及代碼行信息,不包含符號調試信息
    /Z7 C 7.0- Compatible 目標文件或者可執行文件中包含行號和所有符號調試信息,包括變量名及類型,函數及原型等
    /Zi Program Database 創建一個程序庫(PDB),包括類型信息和符號調試信息。
    /ZI Program Database for Edit and Continue 除了前面/Zi的功能外,這個選項允許對代碼進行調試過程中的修改和繼續執行。這個選項同時使#pragma設置的優化功能無效
  • 選擇Link頁,選中複選框"Generate Debug Info",這個選項將使連接器把調試信息寫進可執行文件和DLL
  • 如果C/C++頁中設置了Program Database以上的選項,則Link incrementally可以選擇。選中這個選項,將使程序可以在上一次編譯的基礎上被編譯(即增量編譯),而不必每次都從頭開始編譯。

 斷點
斷點是調試器設置的一個代碼位置。當程序運行到斷點時,程序中斷執行,回到調試器。斷點是 最常用的技巧。調試時,只有設置了斷點並使程序回到調試器,才能對程序進行在線調試。

設置斷點:可以通過下述方法設置一個斷點。首先把光標移動到需要設置斷點的代碼行上,然後

  • 按F9快捷鍵
  • 彈出Breakpoints對話框,方法是按快捷鍵CTRL+B或ALT+F9,或者通過菜單Edit/Breakpoints打開。打開後點擊Break at編輯框的右側的箭頭,選擇 合適的位置信息。一般情況下,直接選擇line xxx就足夠了,如果想設置不是當前位置的斷點,可以選擇Advanced,然後填寫函數、行號和可執行文件信息。

去掉斷點:把光標移動到給定斷點所在的行,再次按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允許被中斷的程序繼續運行、單步運行和運行到指定光標處,分別對應快捷鍵F5、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計算表達式的值。

 

 關注
一個好的程序員不應該把所有的判斷交給編譯器和調試器,應該在程序中自己加以程序保護和錯誤定位,具體措施包括:

  • 對於所有有返回值的函數,都應該檢查返回值,除非你確信這個函數調用絕對不會出錯,或者不關心它是否出錯。
  • 一些函數返回錯誤,需要用其他函數獲得錯誤的具體信息。例如accept返回INVALID_SOCKET表示accept失敗,爲了查明 具體的失敗原因,應該立刻用WSAGetLastError獲得錯誤碼,並針對性的解決問題。
  • 有些函數通過異常機制拋出錯誤,應該用TRY-CATCH語句來檢查錯誤
  • 程序員對於能處理的錯誤,應該自己在底層處理,對於不能處理的,應該報告給用戶讓他們決定怎麼處理。如果程序出了異常, 卻不對返回值和其他機制返回的錯誤信息進行判斷,只能是加大了找錯誤的難度。

另外:VC中要編制程序不應該一開始就寫cpp/h文件,而應該首先創建一個合適的工程。因爲只有這樣,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版下運行程序沒有輸出調試信息來,可這些調試信息卻留在了二進制的可執行文件裏;
我們可以用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:
[email protected]

調用規範與可變參數表

作者:
阿半

  語言調用規範是指進行一次函數調用所採用的傳遞參數的方法,返回值的處理以及調用堆棧的清理。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版本的程序有幫助 。歡迎各位朋友批評指正。


一、該方法適用的範圍
在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。這種由前面語句造成的崩潰根源,在後續程序中方纔顯現出來的情況,顯然用該文所述的方法就無能爲力了。不過在此例中多少還有些蛛絲馬跡可尋找到崩潰的原因:函數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的返回地址給覆蓋 ,從而造成程序崩潰。對於類似的這種造成程序崩潰的錯誤朋友們還有其他方法排錯的話,歡迎一起交流討論。


二、設置編譯產生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版本需要將編譯選項改爲不進行任何優化的選項,否則上述代碼會因爲優化時看作廢代碼而不被編譯,從而看不到崩潰的結果),編譯後產生一個"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;”這正是源文件中產生崩潰的語句。
至此我們僅從崩潰地址就查找出了造成崩潰的源代碼語句和該語句所在源文件中的確切位置,甚至查找到了造成崩潰的編譯後的確切彙編代碼!
怎麼樣,是不是感覺更爽啊?


五、小節

1、新方法同樣要注意可以適用的範圍,即程序由一條語句當即引起的崩潰。另外我不知道除了VC6外,是否還有其他的編譯器能夠產生類似的"cod"文件。
2、我們可以通過比較 新方法產生的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. 

  這種情況我採用一種"試探法"。由於內存分配的塊號不是固定不變的,而是每次運行都是變化的,所以跟蹤起來很麻煩。但是我發現雖然內存分配的塊號是變化的,但是變化的塊號卻總是那幾個,也就是說多運行幾次,內存分配的塊號很可能會重複。因此這就是"試探法"的基礎。

  1. 先在調試狀態下運行幾次程序,觀察內存分配的塊號是哪幾個值;
  2. 選擇出現次數最多的塊號來設斷點,在代碼中設置內存分配斷點: 添加如下一行(對於第 18 個內存分配):
    _crtBreakAlloc = 18; 
    或者,可以使用具有同樣效果的 _CrtSetBreakAlloc 函數:
    _CrtSetBreakAlloc(18); 

     

  3. 在調試狀態下運行序,在斷點停下時,打開"調用堆棧"窗口,找到對應的源代碼處; 
  4. 退出程序,觀察"輸出窗口"的內存泄露報告,看實際內存分配的塊號是不是和預設值相同,如果相同,就找到了;如果不同,就重複步驟3,直到相同。 
  5. 最後就是根據具體情況,在適當的位置釋放所分配的內存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章