由使用LeakDialog時遇到的問題而引出的一些分析
前段時間在使用leakDialog檢測調用malloc和new所分配的記憶體泄露時,發現其根本不起作用!這讓我百思不得其解!周末有時間研究了一下終於弄清了原因所在。本著分享的精神,將其寫成博文,希望對大家有用。
LeakDialog是用於記憶體泄露檢測的常用工具。使用LeakDialog不需要添加任何代碼,就可以捕獲各種形式的記憶體泄露。同時它還能顯示執行記憶體配置的棧回溯以及記憶體配置的統計資訊。
其使用方法很簡單,在此不再介紹它的使用方法,有不懂的可以尋找其他資料。
LeakDialog支援6種不同的分配器:
虛擬記憶體分配器
堆分配器
MPHeap分配器
COM的AllocatorCoTaskmem分配器
COM的私人分配器
C運行時分配器
我們在檢測調用new和malloc分配的記憶體泄露時就是使用了C運行時(CRT C Run Time)分配器。
LeakDialog原理
LeakDialog通過微軟的Detours庫來攔截對記憶體配置和釋放操作的調用,如malloc或free函數。Microsoft Detours庫是在二進位層級對現有代碼進行修改。其主要原理為:在程式執行過程中將所要攔截的函數的起始位置的若干指令替換為一條無條件跳轉指令。該修改是動態修改的,不會修改二進位檔案。 LeakDialog通過改技術攔截每一次的記憶體配置和釋放操作。點擊log按鈕時LeakDialog會找出已經被分配但還未被釋放的記憶體操作。
今天我們僅僅討論LeakDialog的C運行時分配器對調用malloc和new分配的記憶體泄露的檢測。將通過以下幾個測試案例一步步深入挖掘。
實驗一:測試Leakdialog對記憶體泄露的檢測。
使用vc6.0編寫的控制台程式。使用預設配置,主要代碼如下:
#include <iostream>void func2(){ int *p = new int[200]; if(p) { std::cout<<"func2 sucecss!"<<std::endl; }}void func3(){ char *p = (char*)malloc(sizeof(char) * 100); if(p) { std::cout<<"func3 success!"<<std::endl; }}void func1(){ func2(); func3();}int main(){ std::cout<<"by ithzhang---------blog.csdn.net/ithzhang"<<std::endl; getchar(); func1(); getchar(); return 0;}
編譯運行後,使用LeakDialog進行監視,在執行完func1後點擊log按鈕,發現沒有產生記錄檔。這說明LeakDialog沒有檢測到記憶體泄露。通過代碼我們可以發現程式明明出現了兩處記憶體泄露。這說明LeakDialog沒有起作用。相信很多童鞋在初次接觸LeakDialog時都會進行類似的測試。但原因究竟為何?
這裡不賣關子了。由於控制台程式預設配置使用靜態連結CRT(C運行時)庫,而LeakDialog的C運行時分配器僅僅攔截位於msvcrt.dll中的指定的記憶體配置操作。這是導致該問題的罪魁禍首!
選擇工程->設定->C++ ->Code generation 我們看到了工程的預設配置: Use run-time library選項的值為Debugsingle-Thread表示程式在連結時使用c運行庫的靜態版本。如:
要使程式在運行時動態載入CRT庫,只需將Use run-time library修改為Multithreaded DLL或Debug Multithreaded DLL。Debug表示用於調試的dll。如所示:
修改配置後再次執行剛才的操作,這次我們看到log檔案產生了,使用ie開啟後,如:
顯示出了兩次記憶體泄露操作,分別位於func2和func3中。由此我們很容易的找出記憶體泄露的位置。
注意在LeakDialog中配置偵錯符號所在目錄,否則function和filename將會顯示為空白,同時不能選中Use DebugHelp Stack walk API to Walk stacks。
實驗二:驗證LeakDialog原理
1.喚出windbg,選擇open Executable 選擇實驗一產生的控制台程式。由於在本地產生,因此不要配置偵錯符號和原始碼路徑。
2.程式暫停在了位於ntdll.dll中的初始斷點處。輸入bp msvcrt!malloc命令並斷行符號,在位於msvcrt.dll中的malloc函數的入口處設定軟體斷點。
3.按F5繼續執行,並讓程式執行func1.
4.可以看到程式停在了msvcrt.dll的malloc函數入口。
如,我們看到了malloc的彙編代碼。
再次執行上述1-4,只是在2和3之間開啟LeakDialog開啟CRT allocator監視該進程。
這一次程式仍然執行到malloc入口處,如:
比較上述兩圖,我們可以發現第二張malloc的第一條指令被替換成了jmp指令。上述被替換的指令就是LeakDialog為了監視位於msvcrt.dll中的malloc函數而插入的。感興趣的童鞋可以繼續分析jmp指令後的一些操作。
通過同樣的方法我們同樣可以得到對new的調用:
開啟LeakDialog後,operator new函數入口的指令被替換,如:
實驗三:vc2005編譯上述代碼,重複做實驗二(由於vc2005預設配置為動態連結CRT庫,因此不需要修改配置)。
發現並沒有產生log,說明leakdialog並沒有檢測到記憶體泄露。實驗繼續進行。
使用windbg開啟產生的exe。在調用func2之前開啟leakdialog對該進程進行監視,並在msvcr80D.dll(Debug模式下進行,Release下為msvcr80.dll)的malloc處設定斷點。
執行func2,發現程式並未停到斷點處,輸入u msvcrt80D!malloc 對位於msvcrt80D.dll中的malloc進行反組譯碼,發現其第一條指令並未被替換,如:
而接下來的情況更讓人捉摸不透:輸入u msvcrt!malloc 對位於msvcrt.dll中的malloc進行反組譯碼,如:
可以發現位於msvcrt.dll中的malloc的第一條指令被Leakdialog替換了無條件跳轉指令。
查看operator new也發現了類似的情況:
msvcrt.dll中的operator new的替換,如:
這究竟是為何?
參考說明文檔,有下面一句話:
The C Runtime Allocator tracks the following calls from MSVCRT.DLL:
- malloc,
- calloc,
- realloc,
- free,
- new,
- new[],
- delete and
- delete[]
這次終於真相大白,原來Leakdialog僅僅攔截位於msvcrt.dll中的記憶體配置函數。
由於msvcrt.dll是vc6.0使用的CRT庫,因此leakdialog僅僅對使用vc6.0編寫的程式有效。對於由vc2005編譯的使用msvcr80.dll的程式的記憶體泄露,leakdialog無法檢測到。
原因或許就是leakdialog版本太老所致,在其發布時,還沒有vc2005呢吧!
本次實驗結束,你看懂了嗎?歡迎討論。
2014.2.11於浙江杭州