介紹

  在這個調試系列的前一章節我們學習了棧。棧是一個臨時存儲的區域,用於存儲局部變量,參數,返回地址和其它一些編譯器需要的東西。在這一篇當中,我們將學習用戶態下的堆。

 

堆是什麼?

  堆就是當程序需要時,進程空間中用來提供的一大塊內存。當需要時,系統API會分配一塊內存,然後在每塊內存地址前加上一個頭部,表示這塊地址當前正在使用以及它的大小。然後當需要釋放這些內存時,系統會以這個爲依據。(全局變量也在堆中)

  我現在所說的都是用戶態的情況。這一塊內存已經分配在進程空間中分配給你的程序來使用。當特定的庫開始運行時,比如MSVCRT.DLL中的DllMain()Malloc()函數就是以這塊已經分配好的內存來工作的。

  你可以使用HeapCreate來在你的進程中分配一個內存堆。這個函數分配一個堆段,然後返回這個段號。你的程序可以將這個段作爲參數傳遞給HeapAlloc來在這個堆中分配內存。這就是我前面說的malloc的功工作原理,類似於Heap*這樣的函數可以完成所有你分配的段中的內存管理,這也是malloc內部的實現。

  另外還有一個函數VirtualAlloc,這個函數可以在程序的更大的範圍內分配內存,並且能爲內存提交頁。這個函數不需要像HeapAlloc這樣函數預先分配好的堆,並且你甚至可以自己指定地址的位置,但並不推薦。這是更加高級的分配內存的方法,但在大部分程序中不使用,也不需要。

  所以,你也就知道,用一種函數分配內存,而用另一種函數釋放錯誤將是不允許的(例如用LocalAlloc分配,而用C中的free來釋放)。因爲它們使用了不同的方法來分配,並且他們使用的是不同的堆。內部的代碼不知道這種情況,並且總是企圖去釋放它,從而導致崩潰。這也是爲什麼任何模塊分配了內存都需要釋放的原因。如果你在一個DLL中分配了內存,而在另外一個DLL中釋放它,會怎麼樣呢?那有時候會起作用,但是如果有一天你用一個調試版本的DLL替換其中一個,我保證會出現問題。因爲調試版本的堆分配和發佈版本的堆分配包含的信息不同。這是一個壞習慣,我們應該在同一個模塊中分配和釋放。

 

分配的內存不是零初始化的?

  在前一篇教程中,我提到過,棧中會有一些沒有意義的值。棧初始化的時候是全零的,隨着使用會逐漸增添一些數據。並且局部變量並不總是會初始化的,因此如果你進行一次函數調用,這些值並不會重置爲0。如果你將一個值彈出棧,棧指針雖然移動了,但是那個地址的值並不會改變,除非你手動做清理。因此,在棧中看到一些無用的數據是很正常的。相同的事情也會發生在堆中。

  釋放堆中的內存並不會將其中的內容清空爲0,除非在釋放內存前你已經這麼做了。因此,在你分配了內存之後,得到的指針會指向一些垃圾數據,這也就是爲什麼我們在分配了內存後通常都需要將之初始化爲0

  在釋放內存之前將它置爲0看起來是很可笑的一件事,但是,如果你的程序包含的了一些敏感的信息,比如密碼,這樣的工作將會很有意義。因爲你肯定不想自己的程序出錯之後,棧中或堆中存有用戶的密碼。

 

堆問題

  在這篇文章中,我將會講述兩個與堆相關的常見問題,分別是內存泄露和堆損壞。

 

內存泄露

  當你用任務管理器去觀察,並發現你的程序的內存使用在不停的增長,這時你要意識到這可能就是內存泄露。內存泄露的意思就是說你分配了內存,但是忘記了釋放它。當你得到了你分配的內存的地址,但是你卻不小心丟失了它,這就是產生一個泄露。但是程序的內存增長並不就一定代表了內存泄露,有時你的程序確實需要那麼多內存。那麼,我們該如何確定這種情況呢?

  首先,需要做的一件事就是查看任務管理器。如果是很快速的泄露,內存增長速度會非常快;如果是緩慢的泄露,內存會在一段時間內逐漸增長。所以,你需要知道如何去看任務管理器。

1. 虛擬內存 – 這個地方顯示了這個進程所需要的內存。如果這個進程空間被頁換出到磁盤上了,這個值就是你需要的耗費的分頁文件的大小。

2. 內存使用 – 這表示工作集,或者說你所佔有的物理內存的量。有人會很奇怪,爲什麼這個值會比虛擬內存的值還大,這是因爲這個值代表的不是隻有這個進程使用的內存量,還包含了和其他進程共享的內存量。比如,許多進程都會使用kernel32.dll,這個就是進程之間共享的內存量,如果每個進程都複製一份將會很浪費。

  因此,一旦你確定這是一個泄露問題,你就需要找到問題所在。堆的問題是最難追蹤的問題之一。如果你確信這是一個泄露問題,那麼這些泄露可能都來自同一段代碼。即使有很多段代碼都在發生泄露問題,那麼每一段代碼也都會泄露很多次,也就是說泄露的內存中的數據都是相似的。

  既然數據都是相似的,你只要找到那麼大量分配的內存,然後檢查就可以了。下面是一些建議:

1. 一般來說,同一段代碼分配的內存大小都是相同的,這對固定大小的結構體來說是可行的。因此你需要找到那些大小相同的內存分配。但這並不總是正確的。

2. 同一段代碼分配的內存,其中包含的信息或信息的種類應該是相同的或相似的。也就是說,當你找到了這些內存塊,檢查它們是否是否有相似的地方。如果你知道其中包含的數據結構或者其他東西,你可以驗證大小,匹配它們的數據。這將幫助你縮小所要檢查的代碼的範圍。

  我已經寫了一個會造成內存泄露的程序。這是一個很簡單的程序,但是可以幫助你熟悉堆。

  有時候你在程序中發現了內存泄露,這並不意味着那是你寫的代碼造成的問題,如果你使用了第三方庫或DLL,這個問題很可能來自於它們。因此,如果你發現了內存泄露,但是你並沒有自己直接分配過內存,那就有可能你使用了別的東西造成了間接的內存分配。比如,你使用RegOpenKey函數打開一個註冊表鍵,你這道這個函數是如何實現的嗎?不知道的話,你就可能會泄露這個句柄,這個句柄會和其他一些內存有關嗎?會的。句柄泄露的問題我會在後面的教程中再講解。

  我並不是說註冊表鍵的泄露會導致內存使用量的增加,我只是在闡述一個道理,當你使用第三方模塊的時候,可能會間接導致內存泄露,而你是要對此負責的。也就是說,你必須按照函數的使用規則來使用它們,比如最後要使用一些釋放資源的函數。

  我們開始運行這個程序,並發現內存在一直增長。我們來看看是否能找出源頭。首先,找到這個進程的PID,然後使用”CDB –P <PID>”。當然,你也可以使用GUI界面的WinDbg,然後選擇你要調試的進程。當我們在進程中中斷下來後,要做的第一件事情就是”!heap”,顯示進程中的堆。

0:000> !heap

NtGlobalFlag enables following debugging aids for new heaps:    tail checking

    disable coalescing of free blocks

Index   Address  Name      Debugging options enabled

  1:   00140000                 tail checking free checking validate parameters

  2:   00240000                 tail checking free checking validate parameters

  3:   00250000                 tail checking free checking validate parameters

  4:   00320000                 tail checking free checking validate parameters

0:000>

下面,我們來檢查到底哪個堆在造成內存泄露。

0:000> !heap 00140000

Index   Address  Name      Debugging options enabled

  1:   00140000

    Segment at 00140000 to 00240000 (00100000 bytes committed)

    Segment at 00510000 to 00610000 (00100000 bytes committed)

    Segment at 00610000 to 00810000 (00051000 bytes committed)

  2:   00240000

  3:   00250000

  4:   00320000

0:000> !heap 00240000

Index   Address  Name      Debugging options enabled

  1:   00140000

  2:   00240000

    Segment at 00240000 to 00250000 (00006000 bytes committed)

  3:   00250000

  4:   00320000

0:000> !heap 00250000

Index   Address  Name      Debugging options enabled

  1:   00140000

  2:   00240000

  3:   00250000

    Segment at 00250000 to 00260000 (00001000 bytes committed)

  4:   00320000

0:000> !heap 00320000

Index   Address  Name      Debugging options enabled

  1:   00140000

  2:   00240000

  3:   00250000

  4:   00320000

    Segment at 00320000 to 00330000 (00010000 bytes committed)

    Segment at 00410000 to 00510000 (000ee000 bytes committed)

0:000>

我們看看提交內存最多的段(segment)。我們從第一個堆開始。

0:000> !heap 00140000 -a

Index   Address  Name      Debugging options enabled

  1:   00140000

    Segment at 00140000 to 00240000 (00100000 bytes committed)

    Segment at 00510000 to 00610000 (00100000 bytes committed)

    Segment at 00610000 to 00810000 (00051000 bytes committed)

    Flags:                50000062

    ForceFlags:           40000060

    Granularity:          8 bytes

    Segment Reserve:      00400000

    Segment Commit:       00002000

    DeCommit Block Thres: 00000200

    DeCommit Total Thres: 00002000

    Total Free Size:      00000226

    Max. Allocation Size: 7ffdefff

    Lock Variable at:     00140608

    Next TagIndex:        0000

    Maximum TagIndex:     0000

    Tag Entries:          00000000

    PsuedoTag Entries:    00000000

    Virtual Alloc List:   00140050

    UCR FreeList:        00140598

    FreeList Usage:      00040000 00400000 00000000 00000000

    FreeList[ 00 ] at 00140178: 00660118 . 00660118

    Unable to read nt!_HEAP_FREE_ENTRY structure at 00660118

    FreeList[ 12 ] at 00140208: 0023ff78 . 0023ff78

    Unable to read nt!_HEAP_FREE_ENTRY structure at 0023ff78

    FreeList[ 36 ] at 00140328: 0060fe58 . 0060fe58

    Unable to read nt!_HEAP_FREE_ENTRY structure at 0060fe58

    Segment00 at 00140640:

        Flags:           00000000

        Base:            00140000

        First Entry:     00140680

        Last Entry:      00240000

        Total Pages:     00000100

        Total UnCommit:  00000000

        Largest UnCommit:00000000

        UnCommitted Ranges: (0)

 

    Heap entries for Segment00 in Heap 00140000

        00140000: 00000 . 00640 [01] - busy (640)

        00140640: 00640 . 00040 [01] - busy (40)

        00140680: 00040 . 01818 [07] - busy (1800), 

            tail fill - unable to read heap entry extra at 00141e90

        00141e98: 01818 . 00040 [07] - busy (22), 

            tail fill - unable to read heap entry extra at 00141ed0

        00141ed8: 00040 . 00020 [07] - busy (5), 

            tail fill - unable to read heap entry extra at 00141ef0

        00141ef8: 00020 . 002f0 [07] - busy (2d8), 

            tail fill - unable to read heap entry extra at 001421e0

        001421e8: 002f0 . 00330 [07] - busy (314), 

            tail fill - unable to read heap entry extra at 00142510

        00142518: 00330 . 00330 [07] - busy (314), 

            tail fill - unable to read heap entry extra at 00142840

        00142848: 00330 . 00040 [07] - busy (24), 

            tail fill - unable to read heap entry extra at 00142880

        00142888: 00040 . 00040 [07] - busy (24), 

            tail fill - unable to read heap entry extra at 001428c0

        001428c8: 00040 . 00028 [07] - busy (10), 

            tail fill - unable to read heap entry extra at 001428e8

        001428f0: 00028 . 00058 [07] - busy (40), 

            tail fill - unable to read heap entry extra at 00142940

        00142948: 00058 . 00058 [07] - busy (40), 

            tail fill - unable to read heap entry extra at 00142998

        001429a0: 00058 . 00060 [07] - busy (44), 

            tail fill - unable to read heap entry extra at 001429f8

        00142a00: 00060 . 00020 [07] - busy (1), 

            tail fill - unable to read heap entry extra at 00142a18

        00142a20: 00020 . 00028 [07] - busy (10), 

            tail fill - unable to read heap entry extra at 00142a40

        00142a48: 00028 . 00050 [07] - busy (36), 

            tail fill - unable to read heap entry extra at 00142a90

        00142a98: 00050 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00142ca0

        00142ca8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00142eb0

        00142eb8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 001430c0

        001430c8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 001432d0

        001432d8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 001434e0

        001434e8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 001436f0

        001436f8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00143900

        00143908: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00143b10

        00143b18: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00143d20

        00143d28: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00143f30

        00143f38: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00144140

        00144148: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00144350

        00144358: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00144560

        00144568: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00144770

        00144778: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00144980

        00144988: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00144b90

        00144b98: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00144da0

        00144da8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00144fb0

        00144fb8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 001451c0

        001451c8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 001453d0

        001453d8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 001455e0

        001455e8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 001457f0

        001457f8: 00210 . 00210 [07] - busy (1f4), 

            tail fill - unable to read heap entry extra at 00145a00

你可以使用Ctrl+Break來暫停列表的輸出,以方便觀看。從後面幾行,你會發現這些內存塊的大小都是相同的,並且都非常大。這個列表可以如下這樣看:

<ADDRESS>: <Current Size> . <PREVIOUS Size>

我打印出其中一個地址中的值,我們來看看:

0:000> dd 001457f8

001457f8  00420042 001c0700 00006968 00000000

00145808  00000000 00000000 00000000 00000000

00145818  00000000 00000000 00000000 00000000

00145828  00000000 00000000 00000000 00000000

00145838  00000000 00000000 00000000 00000000

00145848  00000000 00000000 00000000 00000000

00145858  00000000 00000000 00000000 00000000

00145868  00000000 00000000 00000000 00000000

第一個DWORD代表的是大小,但是它會分成兩部分,低2個字節代表現在的大小,高兩個字節代表以前的大小。把這個值左移3位就能得到真實的大小。所有的內存分配都是以8爲分配粒度的。42<<3=210,也就是十進制的528。第二個DWORD是標誌(flags),剩下的就是分配給程序的內存。

0:000> dc 001457f8

001457f8  00420042 001c0700 00006968 00000000  B.B.....hi......

00145808  00000000 00000000 00000000 00000000  ................

00145818  00000000 00000000 00000000 00000000  ................

00145828  00000000 00000000 00000000 00000000  ................

00145838  00000000 00000000 00000000 00000000  ................

00145848  00000000 00000000 00000000 00000000  ................

00145858  00000000 00000000 00000000 00000000  ................

00145868  00000000 00000000 00000000 00000000  ................

0:000>

像上面這樣,使用”DC”命令,以字符方式顯示,你會注意到內存中包含”hi”。如果你繼續顯示這些內存的內容,這些分配的528字節的地方都會有”hi”。下面我們來看看下一個堆。

0:000> !heap

NtGlobalFlag enables following debugging aids for new heaps:    tail checking

    disable coalescing of free blocks

Index   Address  Name      Debugging options enabled

  1:   00140000                 tail checking free checking validate parameters

  2:   00240000                 tail checking free checking validate parameters

  3:   00250000                 tail checking free checking validate parameters

  4:   00320000                 tail checking free checking validate parameters

0:000> !heap 00320000 -a

Index   Address  Name      Debugging options enabled

  1:   00140000

  2:   00240000

  3:   00250000

  4:   00320000

    Segment at 00320000 to 00330000 (00010000 bytes committed)

    Segment at 00410000 to 00510000 (000ee000 bytes committed)

    Flags:                50001062

    ForceFlags:           40000060

    Granularity:          8 bytes

    Segment Reserve:      00200000

    Segment Commit:       00002000

    DeCommit Block Thres: 00000200

    DeCommit Total Thres: 00002000

    Total Free Size:      000000b3

    Max. Allocation Size: 7ffdefff

    Lock Variable at:     00320608

    Next TagIndex:        0000

    Maximum TagIndex:     0000

    Tag Entries:          00000000

    PsuedoTag Entries:    00000000

    Virtual Alloc List:   00320050

    UCR FreeList:        00320598

    FreeList Usage:      00000800 00000000 00000000 00000000

    FreeList[ 00 ] at 00320178: 004fdac8 . 004fdac8

    Unable to read nt!_HEAP_FREE_ENTRY structure at 004fdac8

    FreeList[ 0b ] at 003201d0: 0032ffb0 . 0032ffb0

    Unable to read nt!_HEAP_FREE_ENTRY structure at 0032ffb0

    Segment00 at 00320640:

        Flags:           00000000

        Base:            00320000

        First Entry:     00320680

        Last Entry:      00330000

        Total Pages:     00000010

        Total UnCommit:  00000000

        Largest UnCommit:00000000

        UnCommitted Ranges: (0)

 

    Heap entries for Segment00 in Heap 00320000

        00320000: 00000 . 00640 [01] - busy (640)

        00320640: 00640 . 00040 [01] - busy (40)

        00320680: 00040 . 01818 [07] - busy (1800), 

            tail fill - unable to read heap entry extra at 00321e90

        00321e98: 01818 . 000a0 [07] - busy (88), 

            tail fill - unable to read heap entry extra at 00321f30

        00321f38: 000a0 . 00498 [07] - busy (480), 

            tail fill - unable to read heap entry extra at 003223c8

        003223d0: 00498 . 00098 [07] - busy (80), 

            tail fill - unable to read heap entry extra at 00322460

        00322468: 00098 . 00028 [07] - busy (d), 

            tail fill - unable to read heap entry extra at 00322488

        00322490: 00028 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00322568

        00322570: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00322648

        00322650: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00322728

        00322730: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00322808

        00322810: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 003228e8

        003228f0: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 003229c8

        003229d0: 000e0 . 000e8 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00322ab0

        00322ab8: 000e8 . 00238 [07] - busy (220), 

            tail fill - unable to read heap entry extra at 00322ce8

        00322cf0: 00238 . 000a8 [07] - busy (90), 

            tail fill - unable to read heap entry extra at 00322d90

        00322d98: 000a8 . 00058 [07] - busy (3e), 

            tail fill - unable to read heap entry extra at 00322de8

        00322df0: 00058 . 00060 [07] - busy (41), 

            tail fill - unable to read heap entry extra at 00322e48

        00322e50: 00060 . 00050 [07] - busy (31), 

            tail fill - unable to read heap entry extra at 00322e98

        00322ea0: 00050 . 00038 [07] - busy (1b), 

            tail fill - unable to read heap entry extra at 00322ed0

        00322ed8: 00038 . 00040 [07] - busy (26), 

            tail fill - unable to read heap entry extra at 00322f10

        00322f18: 00040 . 00030 [07] - busy (11), 

            tail fill - unable to read heap entry extra at 00322f40

        00322f48: 00030 . 00030 [07] - busy (17), 

            tail fill - unable to read heap entry extra at 00322f70

        00322f78: 00030 . 00028 [07] - busy (d), 

            tail fill - unable to read heap entry extra at 00322f98

        00322fa0: 00028 . 00048 [07] - busy (2f), 

            tail fill - unable to read heap entry extra at 00322fe0

        00322fe8: 00048 . 000d0 [07] - busy (b1), 

            tail fill - unable to read heap entry extra at 003230b0

        003230b8: 000d0 . 00080 [07] - busy (61), 

            tail fill - unable to read heap entry extra at 00323130

        00323138: 00080 . 00038 [07] - busy (1c), 

            tail fill - unable to read heap entry extra at 00323168

        00323170: 00038 . 00048 [07] - busy (2d), 

            tail fill - unable to read heap entry extra at 003231b0

        003231b8: 00048 . 00040 [07] - busy (22), 

            tail fill - unable to read heap entry extra at 003231f0

        003231f8: 00040 . 00030 [07] - busy (17), 

            tail fill - unable to read heap entry extra at 00323220

        00323228: 00030 . 00028 [07] - busy (e), 

            tail fill - unable to read heap entry extra at 00323248       

        00323250: 00028 . 00168 [07] - busy (149), 

            tail fill - unable to read heap entry extra at 003233b0

        003233b8: 00168 . 00058 [07] - busy (39), 

            tail fill - unable to read heap entry extra at 00323408

        00323410: 00058 . 00038 [07] - busy (1b), 

            tail fill - unable to read heap entry extra at 00323440

        00323448: 00038 . 00060 [07] - busy (43), 

            tail fill - unable to read heap entry extra at 003234a0

        003234a8: 00060 . 00030 [07] - busy (12), 

            tail fill - unable to read heap entry extra at 003234d0

        003234d8: 00030 . 00030 [07] - busy (18), 

            tail fill - unable to read heap entry extra at 00323500

        00323508: 00030 . 00038 [07] - busy (1e), 

            tail fill - unable to read heap entry extra at 00323538

        00323540: 00038 . 00028 [07] - busy (c), 

            tail fill - unable to read heap entry extra at 00323560

        00323568: 00028 . 00030 [07] - busy (14), 

            tail fill - unable to read heap entry extra at 00323590

        00323598: 00030 . 00028 [07] - busy (f), 

            tail fill - unable to read heap entry extra at 003235b8

        003235c0: 00028 . 00030 [07] - busy (18), 

            tail fill - unable to read heap entry extra at 003235e8

        003235f0: 00030 . 00040 [07] - busy (28), 

            tail fill - unable to read heap entry extra at 00323628

        00323630: 00040 . 00040 [07] - busy (27), 

            tail fill - unable to read heap entry extra at 00323668

        00323670: 00040 . 00038 [07] - busy (19), 

            tail fill - unable to read heap entry extra at 003236a0

        003236a8: 00038 . 00030 [07] - busy (17), 

            tail fill - unable to read heap entry extra at 003236d0

        003236d8: 00030 . 00050 [07] - busy (34), 

            tail fill - unable to read heap entry extra at 00323720

        00323728: 00050 . 00030 [07] - busy (11), 

            tail fill - unable to read heap entry extra at 00323750

        00323758: 00030 . 00030 [07] - busy (14), 

            tail fill - unable to read heap entry extra at 00323780

        00323788: 00030 . 00068 [07] - busy (4a), 

            tail fill - unable to read heap entry extra at 003237e8

        003237f0: 00068 . 00818 [07] - busy (800), 

            tail fill - unable to read heap entry extra at 00324000

        00324008: 00818 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 003240e0

        003240e8: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 003241c0

        003241c8: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 003242a0

        003242a8: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324380

        00324388: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324460

        00324468: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324540

        00324548: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324620

        00324628: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324700

        00324708: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 003247e0

        003247e8: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 003248c0

        003248c8: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 003249a0

        003249a8: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324a80

        00324a88: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324b60

        00324b68: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324c40

        00324c48: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324d20

        00324d28: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324e00

        00324e08: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324ee0

        00324ee8: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00324fc0

        00324fc8: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 003250a0

        003250a8: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00325180

        00325188: 000e0 . 000e0 [07] - busy (c8), 

            tail fill - unable to read heap entry extra at 00325260

在這個堆中,可以看到很多<address>:e0.e0這樣的分配,這很有可能是一個泄露。下面我們來看看:

0:000> dc 00325188

00325188  001c001c 00180700 66647361 61667361  ........asdfasfa

00325198  73666473 73666461 73666461 61666164  sdfsadfsadfsdafa

003251a8  61736673 73616664 61736664 73616664  sfsadfasdfsadfas

003251b8  66736166 73666473 66736661 66647361  fasfsdfsafsfasdf

003251c8  00617364 baadf00d baadf00d baadf00d  dsa.............

003251d8  baadf00d baadf00d baadf00d baadf00d  ................

003251e8  baadf00d baadf00d baadf00d baadf00d  ................

003251f8  baadf00d baadf00d baadf00d baadf00d  ................

 

除了!heap命令,你還可以從所分配的堆的開始地址一直使用”DC”命令來查看,是否有一些固定的模式。

01c<<3=e0,十進制的224。下面我們來看看源代碼:

  char *p, *x;

  while(1)

  {

     p = malloc(200);

     strcpy(p, "asdfasfasdfsadfsadfsdafasfsadfasdfsadfasfasfsdfsafsfasdfdsa");

 

     x = LocalAlloc(LMEM_ZEROINIT, 500);

     strcpy(x, "hi");

 

     Sleep(1);

  }

很顯然,這裏造成了一個快速的泄露。你們會注意到,分配的是224字節,而不是我們指定的200。這是因爲分配的大小包括了前面說的2DWORD頭,還有必須是8字節爲邊界的。如果你得到了分配給你的內存地址,然後將它減去8,你就可以得到頭部信息了。將分配的這個大小左移3位,然後加上這個地址,你就可以得到下一個分配的內存塊。下面舉例說明:

0:000> dc 0325188 + e0

00325268  001c001c 00180700 66647361 61667361  ........asdfasfa

00325278  73666473 73666461 73666461 61666164  sdfsadfsadfsdafa

00325288  61736673 73616664 61736664 73616664  sfsadfasdfsadfas

00325298  66736166 73666473 66736661 66647361  fasfsdfsafsfasdf

003252a8  00617364 baadf00d baadf00d baadf00d  dsa.............

003252b8  baadf00d baadf00d baadf00d baadf00d  ................

003252c8  baadf00d baadf00d baadf00d baadf00d  ................

003252d8  baadf00d baadf00d baadf00d baadf00d  ................

0:000>

我不會一個一個區比較堆標誌,因爲這是沒有必要的。我覺得最重要的標誌就當前這個內存塊是否分配了。也就是說,當你使用”!heap  <heap>  -a”命令之後,看到了busy,就表示這塊內存沒有被釋放,而free則表示這塊內存已經被釋放了。分配之後標誌爲”00180700”,那麼釋放之後就是”00180400”,你可以寫一個程序測試一下,但是要注意,你釋放之後沒有別的線程啓動或其他的內存分配。

 

私有堆和全局堆

  上面我已經介紹了!heap命令,但是要注意的是,它不會顯示全局堆。如果你有一個全局變量,!heap命令顯示的堆中不會包含這個全局變量。全局堆也不能摧毀,而私有堆可以。

  全局堆還會有損壞的問題,我們將在下一個話題討論。

 

跟蹤內存分配

  還有另外一種跟蹤內存泄露的方法,那就是自己寫一個內存分配函數。如下:

PVOID MyAllocationRoutine(DWORD dwSize)

{

 PVOID pMem = malloc(dwSize + sizeof(_DEBUG_STRUCTURE));

 

 if(pMem)

 {

    _DEBUG_STRUCTURE *pDebugStruc = (_DEBUG_STRUCTURE *)pMem;

 

    /* Fill In Your Debug Information Here */

 

    /* Make Sure You Give the Application the memory AFTER your debug structure */

    pMem = pDebugStruc + 1; 

  }

 

 return pMem;

}

你只要將你自己的頭部加到所有的分配中去就可以了。這也是類似於boundschecker這樣的程序運行的原理。它們能夠將你的分配替換,然後找出是誰分配的這塊內存,並跟蹤它,你也可以這樣做。你甚至可以創建一個全局變量,然後把所有的內存加入到鏈表中去,然後自己寫一個調試擴展將其中的信息都顯示出來。以後的章節我們將講述調試擴展的內容。

  所以,你可以看到,你可以創建自己的函數來分配內存,並向其中加入任何信息。你甚至可以使用#define來講一些函數定義爲你自己想要的,比如LocalAllocmalloc。你還可以在註冊表中增加一些標誌來做這些事情。另外你還要記住寫一個相應的釋放函數。

堆損壞

  堆損壞和內存損壞一樣,基本上就是說所寫的邊界超過了給你分配的大小。造成堆損壞的最常見的原因就是使用了錯誤的方式釋放內存。比如,如果你使用malloc分配內存,而用LocalFree釋放內存。由於它們使用不同的堆,並且甚至使用底層不同的方法來分配和跟蹤內存,而這些釋放函數都會用它們自己的算法來釋放內存,並且不會對所要釋放的堆做一些驗證,這就會造成這裏所說的堆損壞問題。上一節我們用malloc創建了自己的內存分配函數,所以爲了避免這樣的問題,我們應該也創建自己的相應正確的釋放函數。

  導致這個問題的其他原因,也可能是你就是超過了所擁有的內存邊界,比如向內存中寫入了過多的數據,以至於超過邊界,或者是向隨機的一個地址寫入了數據。堆損壞比內存泄露要更難跟蹤。”跟蹤分配的過程”這個策略並不會給你帶來任何幫助,特別是當你重新編譯之後。因爲了你可以能改變了分配的大小,這就可能不會再發生堆損壞的問題,因爲這次堆的大小可能滿足你的條件。

  我寫了一個有一些堆問題的程序。堆問題並不會很快的顯現,這通常需要經過一段時間。當程序中的其他部分使用損壞的內存,或釋放或分配另一個變量的時候,就可能顯現出問題。

  我運行這個程序之後,出現了幾個對話框,告訴你不正確的內存引用。下面就該我們的調試器登場了。

C:\programs\DirectX\Games\src\Games\temp\bin>cdb temp 

 

Microsoft (R) Windows Debugger  Version 6.3.0005.1

Copyright (c) Microsoft Corporation. All rights reserved. 

 

CommandLine: temp

Symbol search path is: SRV*c:\symbols*http://msdl.microsoft.com/download/symbols 

 

Executable search path is:

ModLoad: 00400000 00404000   temp.exe

ModLoad: 77f50000 77ff7000   ntdll.dll

ModLoad: 77e60000 77f46000   C:\WINDOWS.0\system32\kernel32.dll

ModLoad: 77c10000 77c63000   C:\WINDOWS.0\system32\MSVCRT.dll

(a20.710): Break instruction exception - code 80000003 (first chance)

eax=00241eb4 ebx=7ffdf000 ecx=00000004 edx=77f51310 esi=00241eb4 edi=00241f48

eip=77f75a58 esp=0012fb38 ebp=0012fc2c iopl=0         nv up ei pl nz na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202

ntdll!DbgBreakPoint:

77f75a58 cc               int     3

0:000> g

(a20.710): Access violation - code c0000005 (first chance)

First chance exceptions are reported before any exception handling.

This exception may be expected and handled.

eax=61736664 ebx=00000004 ecx=73616664 edx=00142ab8 esi=00142ab8 edi=00140000

eip=77f8452d esp=0012f7e4 ebp=0012f9fc iopl=0         nv up ei pl zr na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00010246

ntdll!RtlAllocateHeapSlowly+0x6bd:

77f8452d 8901             mov     [ecx],eax         ds:0023:73616664=????????

0:000>

這裏我們得到了first chance異常,這通常意味着,如果我鍵入”g”命令,程序將會繼續運行。如果我們得到了”second chance”異常,程序將會出錯暫停。我們看到,這是內存損壞的標誌。記得我們在第一篇教程中說的,遇到這樣的問題,我們要做的第一件事就是找到爲什麼我們會使用這塊內存,誰在使用它,和它在哪裏。那該怎麼辦呢?那就是我們的老朋友了,棧回溯。

0:000> kb

ChildEBP RetAddr  Args to Child

0012f9fc 77f9d959 00140000 50140169 00000006 ntdll!RtlAllocateHeapSlowly+0x6bd

0012fa80 77f83eb1 00140000 50140169 00000006 ntdll!RtlDebugAllocateHeap+0xaf

0012fcac 77f589f2 00140000 40140068 00000006 ntdll!RtlAllocateHeapSlowly+0x41

0012fee4 77e7a6d4 00140000 40140068 00000006 ntdll!RtlAllocateHeap+0xe44

0012ff30 00401024 00000040 00000006 00000000 kernel32!LocalAlloc+0x58

0012ff4c 0040113b 00000001 00322470 00322cf8 temp!main+0x24

0012ffc0 77e814c7 00000000 00000000 7ffdf000 temp!mainCRTStartup+0xe3

0012fff0 00000000 00401058 00000000 78746341 kernel32!BaseProcessStart+0x23

0:000>

可以看到,我們在分配內存,並在NTDLL中發生了錯誤。我們再繼續看,被引用的內存在哪裏。

0:000> u eip - 20

ntdll!RtlAllocateHeapSlowly+0x69d:

77f8450d 058845b356       add     eax,0x56b34588

77f84512 8b7de4           mov     edi,[ebp-0x1c]

77f84515 57               push    edi

77f84516 e85eeaffff       call    ntdll!RtlpUpdateIndexRemoveBlock (77f82f79)

77f8451b 8b4608           mov     eax,[esi+0x8]

77f8451e 89855cffffff     mov     [ebp-0xa4],eax

77f84524 8b4e0c           mov     ecx,[esi+0xc]

77f84527 898d58ffffff     mov     [ebp-0xa8],ecx

0:000> u

ntdll!RtlAllocateHeapSlowly+0x6bd:

77f8452d 8901             mov     [ecx],eax

看上去像是某種鏈表或類似的數據結構。我們看到ECX是作爲一個指針在使用,類似於這樣的語句,DWORD *pECX*pECX=EAX。因此我們需要找出ECX的值從哪來。看到了”ECX, [ESI+0ch]”嗎?等價於下面這條語句:

DWORD *pECX, *pESI; pECX = pESI[12/4];

記住,在彙編語言中沒有類型,因此數組都是以字節來索引的,而不是以數據類型的大小來索引的。因此,我們來Dump[ESI+C]看看。

0:000> dc esi + c

00142ac4  73616664 66736166 73666473 66736661  dfasfasfsdfsafsf

00142ad4  66647361 00617364 feeefeee feeefeee  asdfdsa.........

00142ae4  feeefeee feeefeee feeefeee feeefeee  ................

00142af4  feeefeee feeefeee feeefeee feeefeee  ................

00142b04  feeefeee feeefeee feeefeee feeefeee  ................

00142b14  feeefeee feeefeee feeefeee feeefeee  ................

00142b24  feeefeee feeefeee feeefeee feeefeee  ................

00142b34  feeefeee feeefeee feeefeee feeefeee  ................

錯誤發生的地方如下:

77f8452d 8901  mov  [ecx],eax  ds:0023:73616664=????????

現在就很明瞭了,這是一個字符串,我們在程序裏找找分配這段內存的地方。

x = LocalAlloc(LMEM_ZEROINIT, 5);

strcpy(x, "asdfasfasdfsadfsadfsdafasfsadfasdfsadfasfasfsdfsafsfasdfdsa");

p = LocalAlloc(LMEM_ZEROINIT, 6);

strcpy(p, "hi");

LocalFree(x);

free(p);

  可以看到,我們寫入的數據明顯超過了5個字節。這是很簡單的一個例子。有時,可能就是多寫了一個字節,你必須一點一點往回找,這可能是字符串的null結尾,很多時候你都會忘了要多留一個字節給null。很多時候,我們可能不會注意到這些問題,因爲內存分配是以8爲粒度的。但是有些時候,追蹤這些問題可能就是惡夢,因爲內存一旦超出範圍,罪魁禍首可能就逃之夭夭。

  另外一種方法就是一點一點的跟進程序,如果超出範圍的地方沒有改變,跟進和檢查它很簡單。你可以一步一步減小範圍,這些都是假定我們遇到的都是簡單問題,複雜的話,你必須檢查各個地方。

  你還可以使用斷點來檢查。”ba r1 xxxx”的意思是”如果有人想讀或寫這個地址的話就中斷下來”。如果地址是常量的話,這將會很有用。另外,你還可以降低發生問題的可能性,比如開啓”global flags”等等。

  第一段中說的檢查內存溢出的代碼不能用來檢查內存破壞,這麼說是對的,也是錯的。在當前這樣的情況,你不要期望它能幫你找出內存破壞。但是你做一些調整之後,你可以在分配的內存的開始和結束處放入一些數據,然後你可以釋放這些數據,並檢查這些數據來驗證是否有問題出現,如果發生改變了,就可能有問題出現。這當然是假設損壞是連續的,並且會觸及到邊界,並且不是隨機的一個內存地址。

 

其他工具

還有其他一些工具可以幫助你跟蹤內存損壞和泄漏。

 

Performance Monitor

這是Windows自帶的工具”perfmon”。它可以幫助你監視和記錄系統性能,你可以用它來跟蹤進程中內存的使用軌跡。這個也可以用來發現一些小的泄漏情況。

 

Bounds Checker

這是我上面提到的一個工具。它會在你的程序結束的時候,幫助你跟蹤沒有釋放的內存。當然還可以發現其他種類的泄漏,甚至可以發現內存損壞。這個工具使用非常簡單,並會告訴你哪段代碼分配了內存,但是並沒有釋放,還會告訴你有多少內存沒有釋放。

 

Global Flags

註冊表中的一些選項可以開啓堆檢查和堆驗證的功能。調試工具集中有一個”gflags”工具可以幫助設置這些標誌。以後的教程中我可能會講到這個工具。

 

QuickView:System Explorer

這是我寫的一個工具,工作的Windows2000或更高版本上。它不會得到實時的數據,它只是幫你檢查系統地不同方面來幫助你跟蹤問題。你可以去這個頁面下載安裝程序。

 

總結

這篇教程主要講述了一些用戶太下關於內存泄漏和內存損壞的問題,其中還教了你一些解決問題的方法。

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