誰動了我的指針
2019-11-17 05:22:07
供稿:網(wǎng)友
 
            
  誰動了我的指針?
譯者序:
  本文介紹了一種在調(diào)試過程中尋找懸掛指針(野指針)的方法,這種方法是通過對new和delete運算符的重載來實現(xiàn)的。
  這種方法不是完美的,它是以調(diào)試期的內(nèi)存泄露為代價來實現(xiàn)的,因為文中出現(xiàn)的代碼是絕不能出現(xiàn)在一個最終發(fā)布的軟件產(chǎn)品中的,只能在調(diào)試時使用。
  在VC中,在調(diào)試環(huán)境下,可以簡單的通過把new替換成DEBUG_NEW來實現(xiàn)功能更強更方便的指針檢測,詳情可參考MSDN。DEBUG_NEW的實現(xiàn)思路與本文有相通的地方,因此文章中介紹的方法雖然不是最佳的,但還算實用,更重要的是,它提供給我們一種新的思路。 
簡介:
  前幾天發(fā)生了這樣一件事,我正在調(diào)試一個程序,這個程序用了一大堆亂七八糟的指針來處理一個鏈表,最終在一個指向鏈表結(jié)點的指針上出了問題。我們預(yù)計它應(yīng)當(dāng)指向的是一個虛基類的對象。我想到第一個問題是:指針?biāo)傅牡胤秸娴挠幸粋€對象嗎?出問題的指針值可以被4整除,并且不是NULL的,所以可以斷定它曾經(jīng)是一個有效的指針。通過使用Visual Studio的內(nèi)存查看窗口(View->Debug Windows->Memory)我們發(fā)現(xiàn)這個指針?biāo)傅臄?shù)據(jù)是FE EE FE EE FE EE ...這通常意味著內(nèi)存是曾經(jīng)是被分配了的,但現(xiàn)在卻處于一種未分配的狀態(tài)。不知是誰、在什么地方把我的指針?biāo)傅膬?nèi)存區(qū)域給釋放掉了。我想要找出一種方案來查出我的數(shù)據(jù)到底是怎么會被釋放的。
背景:
  我最終通過重載了new和delete運算符找到了我丟失的數(shù)據(jù)。當(dāng)一個函數(shù)被調(diào)用時,參數(shù)會首先被壓到棧上后,然后返回地址也會被壓到棧上。我們可以在new和delete運算符的函數(shù)中把這些信息從棧上提取出來,幫助我們調(diào)試程序。
代碼:
  在經(jīng)歷了幾次錯誤的猜測后,我決定求助于重載new和delete運算符來幫我找到我的指針?biāo)赶虻臄?shù)據(jù)。下面的new運算符的實現(xiàn)把返回地址從棧上提了出來。這個返回地址位于傳遞過來的參數(shù)和第一個局部變量的地址之間。編譯器的設(shè)置、調(diào)用函數(shù)的方法、計算機的體系結(jié)構(gòu)都會引響到這個返回地址的實際位置,所以您在使用下面代碼的時候,要根據(jù)您的實際情況做一些調(diào)整。一旦new運算符獲得了返回地址,它就在將要實際分配的內(nèi)存前面分配額外的16個字節(jié)的空間來存放這個返回地址和實際的分配的內(nèi)存大小,并且把實際要分配的內(nèi)存塊首地址返回。
  對于delete運算符,你可以看到,它不再釋放空間。它用與new同樣的方法把返回地址提取出來,寫到實際分配空間大小的后面(譯者注:就是上面分配的16個字節(jié)的第9到第12個字節(jié)),在最后四個字節(jié)中填上DE AD BE EF(譯者注:四個十六進制數(shù),當(dāng)成單詞來看正好是dead beef,用來表示內(nèi)存已釋放真是很形象!),并且把剩余的空間(譯者注:就是原本實際應(yīng)該分配而現(xiàn)在應(yīng)該要釋放掉的空間)都填上一個重復(fù)的值。
  現(xiàn)在,假如程序由于一個錯誤的指針而出錯,我只需打開內(nèi)存查看窗口,找到出錯的指針?biāo)傅牡胤剑偻罢?6個字節(jié)。這里的值就是調(diào)用new運算符的地址,接下來四個字節(jié)就是實際分配的內(nèi)存大小,第三個四個字節(jié)是調(diào)用delete運算符的地址,最后四個字節(jié)應(yīng)該是DE AD BE EF。接下的實際分配過的內(nèi)存內(nèi)容應(yīng)該是77 77 77 77。
  要通過這兩個返回地址在源程序中分別找到對應(yīng)的new和delete,可以這樣做:首先把表示地址的四個字節(jié)的內(nèi)容倒序排一下,這樣才能得到真正的地址,這里因為在Intel平臺上字節(jié)序是低位在前的。下一步,在源代碼上右擊點擊,選“Go To Diassembly”。在反匯編的窗口上的左邊一欄就是機器代碼對應(yīng)的內(nèi)存地址。按Ctrl + G或選擇Edit->Go To...并輸入你找到的地址之一。反匯編的窗口就將滾動到對應(yīng)的new或delete的函數(shù)調(diào)用位置。要回到源程序只需再次右鍵單擊,選擇“Go To Source”。您就可以看到相應(yīng)的new或delete的調(diào)用了。
  現(xiàn)在您就可以很方便的找出您的數(shù)據(jù)是何時丟失的了。至于要找出為什么delete會被調(diào)用,就要靠您自己了。
  #include <MALLOC.H>
  void * :perator new(size_t size)
  {
    int stackVar;
    unsigned long stackVarAddr = (unsigned long)&stackVar;
    unsigned long argAddr = (unsigned long)&size;
    void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2);
    void * retAddr = * retAddrAddr;
    unsigned char *retBuffer = (unsigned char*)malloc(size + 16);
    memset(retBuffer, 0, 16);
    memcpy(retBuffer, &retAddr, sizeof(retAddr));
    memcpy(retBuffer + 4, &size, sizeof(size));
    return retBuffer + 16;
  }
  void :perator delete(void *buf)
  {
    int stackVar;
    if(!buf)
      return;
    unsigned long stackVarAddr = (unsigned long)&stackVar;
    unsigned long argAddr = (unsigned long)&buf;
    void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2);
    void * retAddr = * retAddrAddr;
    unsigned char* buf2 = (unsigned char*)buf;
    buf2 -= 8;
    memcpy(buf2, &retAddr, sizeof(retAddr));
    size_t size;
    buf2 -= 4;
    memcpy(&size, buf2, sizeof(buf2));
    buf2 += 8;
    buf2[0] = 0xde;
    buf2[1] = 0xad;
    buf2[2] = 0xbe;
    buf2[3] = 0xef;
    
    buf2 += 4;
    memset(buf2, 0x7777, size);
    // deallocating destroys saved addresses, so dont
    // buf -= 16;
    // free(buf);
  }
其它值得關(guān)注的地方:
  這段代碼同樣可以用于內(nèi)存泄露的檢測。只需修改delete運算符使它真正的去釋放內(nèi)存,并且在程序退出前,用__heapwalk遍歷所有已分配的內(nèi)存塊并把調(diào)用new的地址提取出來,這就將得到一份沒有被delete匹配的new調(diào)用列表。
  還要注重的是:這里列出的代碼只能在調(diào)試的時候去使用,假如你把它段代碼放到最終的產(chǎn)品中,會導(dǎo)致程序運行時內(nèi)存被大量的消耗。