誰動了我的指標? (轉)

amyz發表於2007-08-14
誰動了我的指標? (轉)[@more@]

誰動了我的指標?

譯者序:
  本文介紹了一種在過程中尋找懸掛指標(野指標)的方法,這種方法是透過對new和delete運算子的過載來實現的。
  這種方法不是完美的,它是以除錯期的洩露為代價來實現的,因為文中出現的程式碼是絕不能出現在一個最終釋出的產品中的,只能在除錯時使用。
  在VC中,在除錯環境下,可以簡單的透過把new替換成DE_NEW來實現功能更強更方便的指標檢測,詳情可參考MSDN。DEBUG_NEW的實現思路與本文有相通的地方,因此文章中介紹的方法雖然不是最佳的,但還算實用,更重要的是,它提供給我們一種新的思路。

簡介:
  前幾天發生了這樣一件事,我正在除錯一個,這個程式用了一大堆亂七八糟的指標來處理一個連結串列,最終在一個指向連結串列結點的指標上出了問題。我們預計它應當指向的是一個虛基類的。我想到第一個問題是:指標所指的地方真的有一個物件嗎?出問題的指標值可以被4整除,並且不是NULL的,所以可以斷定它曾經是一個有效的指標。透過使用的記憶體檢視視窗(View->Debug ->Memory)我們發現這個指標所指的資料是FE EE FE EE FE EE ...這通常意味著記憶體是曾經是被分配了的,但現在卻處於一種未分配的狀態。不知是誰、在什麼地方把我的指標所指的記憶體區域給釋放掉了。我想要找出一種方案來查出我的資料到底是怎麼會被釋放的。

背景:
  我最終透過過載了new和delete運算子找到了我丟失的資料。當一個被時,引數會首先被壓到棧上後,然後返回地址也會被壓到棧上。我們可以在new和delete運算子的函式中把這些資訊從棧上提取出來,幫助我們除錯程式。

程式碼:
  在經歷了幾次錯誤的猜測後,我決定求助於過載new和delete運算子來幫我找到我的指標所指向的資料。下面的new運算子的實現把返回地址從棧上提了出來。這個返回地址位於傳遞過來的引數和第一個區域性變數的地址之間。的設定、呼叫函式的方法、的體系結構都會引響到這個返回地址的實際位置,所以您在使用下面程式碼的時候,要根據您的實際情況做一些調整。一旦new運算子獲得了返回地址,它就在將要實際分配的記憶體前面分配額外的16個位元組的空間來存放這個返回地址和實際的分配的記憶體大小,並且把實際要分配的記憶體塊首地址返回。
  對於delete運算子,你可以看到,它不再釋放空間。它用與new同樣的方法把返回地址提取出來,寫到實際分配空間大小的後面(譯者注:就是上面分配的16個位元組的第9到第12個位元組),在最後四個位元組中填上DE AD BE EF(譯者注:四個十六進位制數,當成單詞來看正好是dead beef,用來表示記憶體已釋放真是很形象!),並且把剩餘的空間(譯者注:就是原本實際應該分配而現在應該要釋放掉的空間)都填上一個重複的值。
  現在,如果程式由於一個錯誤的指標而出錯,我只需開啟記憶體檢視視窗,找到出錯的指標所指的地方,再往前找16個位元組。這裡的值就是呼叫new運算子的地址,接下來四個位元組就是實際分配的記憶體大小,第三個四個位元組是呼叫delete運算子的地址,最後四個位元組應該是DE AD BE EF。接下的實際分配過的記憶體內容應該是77 77 77 77。
  要透過這兩個返回地址在源程式中分別找到對應的new和delete,可以這樣做:首先把表示地址的四個位元組的內容倒序排一下,這樣才能得到真正的地址,這裡因為在平臺上位元組序是低位在前的。下一步,在上右擊點選,選“Go To Diassembly”。在反的視窗上的左邊一欄就是機器程式碼對應的記憶體地址。按Ctrl + G或選擇Edit->Go To...並輸入你找到的地址之一。反彙編的視窗就將滾動到對應的new或delete的函式呼叫位置。要回到源程式只需再次右鍵單擊,選擇“Go To ”。您就可以看到相應的new或delete的呼叫了。
  現在您就可以很方便的找出您的資料是何時丟失的了。至於要找出為什麼delete會被呼叫,就要靠您自己了。
  #include

  void * ::operator 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 ::operator 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 don't
    // buf -= 16;
    // free(buf);
  }

其它值得關注的地方:
  這段程式碼同樣可以用於記憶體洩露的檢測。只需修改delete運算子使它真正的去釋放記憶體,並且在程式退出前,用__heapwalk遍歷所有已分配的記憶體塊並把呼叫new的地址提取出來,這就將得到一份沒有被delete匹配的new呼叫列表。
  還要注意的是:這裡列出的程式碼只能在除錯的時候去使用,如果你把它段程式碼放到最終的產品中,會導致程式執行時記憶體被大量的消耗。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-956795/,如需轉載,請註明出處,否則將追究法律責任。

相關文章