程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 資源洩露檢測《續》

資源洩露檢測《續》

編輯:關於C語言

上次做了一個內存洩露檢測的工具,可以在系統退出的時候檢測是否發生內存洩露,並打印出洩露內存處的函數調用堆棧,該工具對於發現的洩露的程序確實能夠快速的定位到洩露發生的函數調用位置,但是人總是懶惰的動物,使用了幾次後發現用起來實在是有點不爽,不爽點主要有: 1、  每次申請內存的時候都記錄了調用堆棧,這個時候有多次分配內存操作用來保存文件名、函數名、行號等信息,但是這些信息用起來卻非常的少,因為發生內存洩露的可能性還是比較低的,這樣使用該工具後程序的性能大打折扣,對於數據庫這樣的軟件來說,非常影響測試的效率。 2、  源代碼的控制並不在測試部門,因此如果要進行內存洩露的測試,則每次拿到源代碼之後,都需要修改代碼,而且修改起來比較繁瑣,比如要初始化,記錄內存指針和大小,釋放的時候要從鏈表中將其刪除,選擇合適的位置將洩露的內存打印出來。第一次還很新鮮,第二次開始嫌麻煩了,還出了點錯,第三次則決定將這個方法放棄了,這實在太麻煩了,而且程序裡面還不一定會有問題。 上述理由應該足夠支持我對內存洩露檢測工具進行優化了,期望達到的效果就是在對程序改動盡可能小的情況下,可以配置的對程序內存洩露問題進行檢測。 第一想法肯定就是用鉤子截獲內存分配和釋放函數,這樣在內存分配完後將指針和大小記錄下來,釋放的時候則將其刪除掉,最後剩下的就是有內存洩露的地方了。接下來就是研究如何截獲程序內的函數了,Windows下面提供了專門的hook函數,但是只能截獲消息,程序內的函數調用並沒有消息的通信,這個方法基本上被否定了,在《編程高手箴言》中,有講述WindowsC的掛鉤,這裡截取其中部分文字說明下。Windows中,所有編譯出來的程序都有一個ImportImport中有一個JMP表,所有的函數調用時,先會跳到Import表中,再通過JMP跳到對應的執行函數。所以如果要掛鉤的話,只需要把JMP的地址換成要掛鉤的函數的地址即可。別人調用函數時就會JMP到掛鉤的函數處。 OK,實踐一下看看Windows下面函數調用到底是怎麼回事吧,寫個簡單的程序。 #include <stdlib.h> #include <stdio.h>  void test1() {        printf("call test1\n");        return; }  void test2() {        printf("call test2\n");        return; } void main() {        test1(); }test1的調用處設置一個斷點,調試到此處,查看一下匯編代碼: call        @ILT+10(test) (0040100f)test1的調用是call了一個@ILT+10ILT的意思就是Incremental Link Table只在Debug版本下才有),即test1函數對應的是ILT偏移10處的JMP指令,即call跳轉到地址0040100f 處,OK,按F11跟進去看看是什麼情況吧。 0040100F   jmp         test1 (00401020) 00401014   jmp         test2 (00401070) 在地址0040100f 處果然有一個JMP指令,它是跳轉到00401020處,另外test2函數的JMP指令也在這裡哦。看看00401020內存是什麼吧,按F11繼續跟進,發現我們終於來到了test1函數定義的地方了,首先肯定還是一些函數調用最基本的壓棧操作什麼的,這裡就不細說了,對我們hook並沒有什麼用處。 現在知道函數調用的基本方式了:call 然後JMP,要掛鉤的話,我們只需要修改ILTjmp的地址即可,例如上面,如果要hook函數test1使得其跳轉到test2中去,只需要將jmp後面的地址修改為test2的地址即可。但是要注意的是,這塊內存可不是隨隨便便就可以讓你改的,不然不小心寫錯內存地址,Windows就慘了,但是Windows也不是那麼絕情啊,提供了函數來讓一個WriteProcessMemory的函數來讓你寫EXE的內存,也就是說你很清楚自己在做什麼了。另外有點要說明一下,在debug下,當把斷點設置到test1()調用處的時候,watch一下test1,發現其值是00401020,也就是函數定義的位置,但是如果我們將test1打印出來的話,卻發現是0040100F,也就是test1ILT中的位置,這個剛開始走了不少彎路才發現。 寫個簡單的程序試下我們的想法是否能夠行得通吧。 void hookfunc() {        LPBYTE lpByte1;        LPBYTE lpByte2;        DWORD dwAddr1;               lpByte1 = (LPBYTE)test1;       //Get old function JMP Addr        lpByte1 = (LPBYTE)&lpByte1[1];        lpByte2 = (LPBYTE)test2;       //Get new function JMP Addr        lpByte2 = (LPBYTE)&lpByte2[1];       //get new and old function's addr        memcpy(&dwAddr1, lpByte2, sizeof(DWORD));        WriteProcessMemory(GetCurrentProcess(), lpByte1, &dwAddr1,sizeof(DWORD), NULL); } 這個程序首先獲得test1test2函數的地址,也就是ILT中的內存地址,JMP指令占用了一個字節,後面JMP的地址是一個DWORD,所以先跳過JMP的一個字節,將指針指向JMP後面的地址,將test2函數的地址拷貝出來,用WriteProcessMemory函數將test2JMP地址寫入到test1JMP地址中,調試運行下吧,很不幸,程序掛了。上面的想法似乎哪裡出問題了?還是繼續跟蹤一下吧,在test1的調用處設置斷點,跟蹤到ILT中,發現:00401005   jmp         test1+4Bh (0040107b)0040100A   jmp         test2 (00401080) JMP指令和test2處的還是不同,後面的地址並不是我們預想的test2的地址,理論上此處應該是00401070才符合我們的想法,看來上面的想法還是有問題,不如把JMP的值打印出來看看吧,分別打印test1test2後面JMP的地址發現,一個是0X26一個是0X71,這個肯定不會是函數的絕對地址了,看來跳轉的是一個相對地址,真是笨啊,居然把匯編的知識給忘了,點擊右鍵打開Code Bytes,再來看看吧: 00401005 E9 26 00 00 00       jmp         test1 (00401030) 這條JMP指令的16進制形式為E9 26 00 00 00,而E9是遠距離跳轉,即此處的跳轉到的地址為:00401005 + 5本條指令的長度)+ 26 = 00401030,而00401030test1函數的真實的地址。現在疑團都解開了,按照這個邏輯,我們上面修改過後,實際跳轉的位置就應該是00401005 + 5 + 71 = 0040107B 這個並不是test2函數的真實地址。所以上面的程序還要做下改動,即計算出JMP test1JMP test2兩條指令內存地址的偏移,調整之後應該是: 00401005 + 5 + 0040100A - 00401005+ 71 = 0040108000401080就是test2函數的真實地址。因此上面dwAddr1的值應該按照上面的公式計算出一個偏移量,即71 + 0040100A - 00401005= 76 再試一下,是不是發現調用test1,打印出來的卻是call test2 Hook函數test1成功了! 但是現在卻出現了另外一個問題,如果要調用真實的test1函數怎麼辦呢?現在我們在ILT中已經沒有test1JMP指令了,一種辦法是再創建一個空的函數,把這個函數在ILT中的JMP指令修改為JMP test1去,調用那個空的函數就等於是調用了真實的test1了,另外一種辦法就是把JMP test2修改成JMP test1,即交換test1test2的調用。第二種辦法似乎更加合適點,因為我們有理由這麼假設,test2函數是一個鉤子函數,我們一般情況下肯定不會直接來調用test2,而是通過hook的方式來調用到test2的,所以,把test1test2ILT中的指令交換之後,顯式的調用test1的話就會跳轉到test2函數,顯式的調用test2函數的話則跳轉到test1函數。另外還有一種辦法就是記錄下test1的絕對地址,自己通過匯編代碼來直接調用,這個相對來說就麻煩多了,沒有試驗。 另外有一點要說的就是,對於WindowsAPI函數,debug下也並不會產生類似的JMP指令,而是直接通過CALL指令跳轉到該API函數的地址,在《編程高手箴言》中對這種情況進行了處理,其方法是通過申請一個全局變量,使得其仍然按照JMP的方式來調用,然後修改JMP的地址就可以了。 至此我們已經實現了掛接本地進程內部函數,從根本上解決了內存洩露程序優化的最大障礙。接下來考慮另外一個問題,我們是否直接提供源代碼,給一個初始化的函數,將所有這些代碼加入到工程中去,然後調用初始化函數來hook?相比於最開始的版本需要對所有內存分配、釋放代碼都做改動,這個已經進步了很多,但是似乎還不是那麼的透明,能否做得再方便一點呢?能不能做出動態鏈接庫的形式,然後隱式的調用初始化函數?這樣用戶只需要在程序編譯的時候加載.lib文件即可。這裡需要提一下#pragma指令的一個用法: #pragma comment(linker,”/include:… pragma comment指令將一個注釋記錄放入一個對象文件或可執行文件中,最常用的就是#pragma comment(lib,”ws2_32.lib”)這個指令告訴編譯器將ws2_32.lib庫文件鏈接到目標文件中。而linker的作用則是將一個鏈接選項放入目標文件中,/include則可以強制包含某個對象。因此我們可以在DLL中創建一個用來初始化的類,並聲明一個該類的全局對象,例如__declspec(dllexport) ResourceLeakDetector rld; rld對象導出,在rld.h的頭文件中,加上一條pragma指令:#pragma comment(linker, "/include:__imp_?rld@@3VResourceLeakDetector@@A") 用來強制包含rld對象,後面的@符合是因為我們是用c++方式導出的,而__imp_的意思則是使用導入對象的一個前綴。為了方便用戶使用,我們另外在rld.h頭文件中再加入一個:#pragma comment(lib, "rld.lib") 至此我們要使用內存洩露檢測程序只需要在工程中加入rld.h,然後隨便在哪個文件裡面將rld.h include盡量就可以了。 最後就是調用堆棧的效率問題,因為我們在內存洩露情況發生很少的前提下,每次函數調用的時候都獲取調用堆棧並將文件名、函數名解析出來保存是很耗資源的事情,一個比較合理的方法就是只獲取函數的偏移地址,而不去解析其文件名、函數名。在最後有洩露發生的地方再根據偏移地址來解析文件名、函數名。 最後出來的內存洩露檢測程序在運行效率上比原始的版本提升了很多,而且使用起來也不是那麼的復雜了:)。 另外,對於路徑中包含中文名的話,以前顯示會截斷字符,究其原因是很多機器上安裝的DbgHelp.dll版本太老,沒有提供解析路徑為寬字符的函數,從windows網站下載了最新的Debug工具庫之後,該問題也已經解決。關於Vista下無法獲得調用堆棧的問題也隨之解決了。

本文出自 “越測越開心” 博客,請務必保留此出處http://happytest.blog.51cto.com/324097/62791

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved