调试指南3 堆以及堆上常见的问题

 
简介
在前一部分中我们了解了关于堆栈的知识。堆栈是为本地变量、参数、返回值准备的临时存储区。这一部分里,我们将了解一下堆在用户模式下是怎样一个东西。
什么是堆?
堆是进程空间内的一块存储区,应用程序在需要时可以使用系统API来进行内存申请。系统API会在被申请过的内存位置上加上一个头来标记它们是否正在被使用的,以及他们的大小。当系统释放这些内存时就会使用这些参数。(全局变量也是保存在堆中)。
我上面所说的前提是在用户模式下。当加载某个链接库(比如,MSVCRT.DLL的DllMain())的时候就已经将这块存储区分配到进程空间中。程序可以使用malloc()对这块内存进行操作。
内存堆是通过一些API函数来分配到进程空间的,比如HeapCreate。这个函数会分配一个堆段然后返回它的段号。应用程序将这个段号传递给HeapAlloc来从堆中分配内存。这和我刚才提到的malloc完成的功能是一样的。Heap*类函数能够完成所有对堆的管理,malloc内部其实也是调用了这类函数。
还有另外一个函数VirtualAlloc。这个函数可以在更大范围上帮助应用程序分配内存,并且它是直接向页提交。也就是说它不需要象HeapAlloc那样从预先分配的堆中分配,你甚至可以指定内存的位置但这样是没有必要的。这个函数是更高级的内存分配方法,大多时候不需要为应用程序使用这个函数。
上面也是释放内存时不能使用与分配内存函数不配对的函数来释放的原因:不同函数分配内存时可能用了不同的方法或不同的堆,而释放函数只是简单的执行释放操作,这样就会导致内存崩溃。也正因为如此,一个模块内分配的内存要在这个模块内释放。否则会怎么样呢?有时这可能不会导致问题,但假如有一天你想将其中一个DLL替换为Debug版本,我保证将会出现问题。因为Debug和Release版本的堆分配是不一样的。这(在不同DLL分配和释放)绝对是一个坏习惯。所以一定要记住:分配和释放必须在同一个模块进行并且使用同一对函数。
分配的内存不是空的?
在上一篇指南中我提到:在栈上可能会有毫无意义的数据(比如栈上有一个返回地址但这个函数调用其实并没有发生过等等)。栈在开始的时候会被初始化为0,但随着使用,它会变得很脏。因为本地变量并不总被初始化,在完成一次函数调用后,栈被移除的时候这些值并不会被清0。执行一次出栈操作只是将栈的规模缩小,但栈上的值仍然存在除非物理的将它们清除。有时候,栈操作会被优化,这时候也不会清除栈上的变量。所以在栈上见到幽灵数据很正常,当然同样的情况也存在于堆上。
除非之前人为的执行清除,否则在堆上释放某个位置的内存不会将它置为0。所以,刚刚分配的内存,内存指针却指向一对垃圾,也是很正常的。这正是每次分配内存后都要将它清0的原因。
释放内存之前将内存清0也许有些可笑,但如果里面包含了敏感信息,比如密码,或者类似的什么东西,你最好这样做。这样才能将这些信息清除。当我的程序崩溃时,我可不想让别人在堆或栈上找到用户的密码。
堆操作问题
这篇文章中,我将介绍堆操作中最常见的问题――内存泄漏和堆破坏。
内存泄漏
有时候,我们会发现我们的程序的内存使用量随着运行时间在逐渐增加(可以用任务管理器看到),这时候就说明程序中出现了内存泄漏。内存泄漏意思是,程序在不停的分配内存但没有释放。当程序落掉一些分配的空间不再使用它们甚至忘掉它们的存在时,内存泄漏就发生了。这是真正的泄漏,但也许程序确实需要不断申请内存,这时候并没有泄漏,我们怎么区分呢?
第一件要做的就是查看一下任务管理器。如果是快速的泄漏,就能看到内存增长的很快。如果是慢速的泄漏,内存会随着时间慢慢增长。首先,你应该知道怎么使用任务管理器。
1. Virtual Memory-这个域显示了进程独占的内存量。如果进程将被交换到磁盘,它就显示了在页面文件中占据的存储空间。
2. Memory Usage-这个域显示了进程使用了多少物理内存。有时候它会比Virtual Memory大,这是因为它不仅包含进程独占的内存汉包含与其它进程共享的内存。比如,许多进程使用kernael32.dll,而将同一段执行代码复制到每个进程空间是很浪费的。
当确定程序中有内存泄漏后,就需要找到问题的所在。而堆中出现的问题是最难跟踪到的。这里有一个窍门:如果是内存泄漏,这些内存分配通常来自统一个地方,即便有很多地方在分配内存,它们也会不停的分配很多内存。也就是说,通常存储的数据是相似的。
这样你置需要找到分配大量内存的地方,检查这些地方。这里还有些技巧:
1. 通常来说,出自同一个地方的内存分配会有相同的大小。对于固定大小的结构这样是不错的,所以只需找到分配了许多同样内存的地方。但对于动态分配的内存就不是这样了。
2. 出自同一个地方的内存通常有相同或近似的类型或信息。也就是说,比较不同地方的内存分配看它们是否有相似的地方。如果你了解程序的数据结构你就能猜到内存分配出自哪里。这可以帮助你缩小搜索范围。
我这里有一个小程序来模拟内存泄漏。下面我们将会跟踪一下这个泄漏,来帮助你熟悉堆。
其实,当我们从任务管理器中看到我们的程序发生内存泄漏时,这泄漏并不一定出自我们的代码。如果程序中使用了第三方的DLL,泄漏也许是发生在DLL中。如果你看到程序出现了内存泄漏但你并没有直接的分配内存,那有可能你在间接的分配内存。比如,当使用RegOpenKey打开一个注册表项时,你知道它内部是怎么实现的吗?肯定不能。所以只能漏掉这个“Handle”。而这里面是否会有内存分配呢?也许会有。关于“Handle”泄漏的问题我们会在后面的指南中见到。
我并不是说漏掉一个注册表项会导致内存增长,也并不是说,其它模块中出现的内存泄漏会以句柄泄漏的形式出现。我只是想说,与其它模块交互时可能会间接的分配内存,而这些内存也必须由你来负责释放。如果有些库明确说明需要调用free或其它销毁函数,我们必须遵守这个说明。
现在来运行我们的小程序,我们会发现内存增长了。接下来我们看看能不能找到它在哪发生的。首先找到进程的PID,然后运行“CDB –P<PID>”。当然,也可以用WinDbg,从UI中选择这个进程。进入以后,执行“!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>
黑体部分是申请最多内存的段。然后一个一个来看。
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
为了将列表缩短,我点击了几下“Control Break”来跳出列表。我们可以注意到,后面的内存分配都是一样的大小,而且都比较大。列表每一个域的意义如下:
<ADDRESS>: <Current Size> . <PREVIOUS Size>
如果将某一个地址dump出来,情形如下:
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值代表分配的大小。但它被分成了两部分。低字代表当前的大小,高字代表以前的大小。所以为了得到真正的大小需要将这个值左移3位,因为内存是以8位粒度分配的,所以42<<3=210或者528。第二个DOWORD是标志位,剩下的就是程序分配的内存。
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”来dump的话,就可以看到,申请的内存中包含了“hi”。如果继续dump的话,每隔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的内存分配。这就有可能是泄漏。如果用“dc”看一下的话,它们都是一样的:
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字节。这是因为分配的尺寸中包括了2 DWORD的头,它也是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;
}
这里只是在内存分配时加入了自己的头。这个头信息还可以更复杂些,比如,还可以包括返回地址、分配者以及线程ID等。其实boundschecker就是这样做的。它们会替换掉内存分配函数然后用实现内存跟踪。我们也可以自己实现这个功能,我们甚至可以创建一个全局变量然后把所有分配的内存都添加进一个链表中,这样就可以生成一个扩展的调试帮助工具来查看所有已分配的内存的信息。我们将在下一篇指南中涉及到这个问题。
如上所示,可以创建自己的内存分配函数在分配的空间中加入有用的信息来帮助我们跟踪内存泄漏或者是内存破坏。还可以使用#define来规定只有在某种模式下才使用自己的代码,比如debug下。这样,我们用普通的编译方式就可以重定义LocalAlloc或者malloc。如果想在任何情况下都一直使用自己的函数可以通过修改注册表的方法来实现。总之,一切皆有可能。
一定要记得创建自己的free函数。这个函数首先减掉自定义的结构的大小,然后再调用free API。
堆破坏
当变量写入超过内存边界时就会出现堆破坏或者叫内存破坏。当使用错误的方法来释放内存时就很有可能出现这种情况。比如,如果你使用malloc()分配了一段内存,然后使用LocalFree()进行释放。因为它们使用不同的堆,也可能使用了不同的实现方法来分配和跟踪内存。问题出自这些函数试图用它们原来的算法来释放内存而不会去做任何检查。就那上面的代码为例,我们使用了自己的malloc方法来分配内存,其它API也会有同样的问题,所以一定要使用与分配内存函数对应的释放函数来释放内存。
另外一个常见的问题就是使用内存时超过了边界。比如,写入的空间尺寸大于分配的尺寸或者随机的向不属于本程序的内存区写入数据。堆破坏问题比泄漏更难追踪到,原来那些跟踪策略不会太奏效。当你重新编译试图用带跟踪信息的分配函数重现问题时,问题可能就不会出现了。这是因为,这个时候更改了内存的尺寸,更改的尺寸可能已经足够使用了,刚才那个问题就不会再发生了。
我这里有一个小程序,它暴露了一些堆的问题。堆问题不会在程序运行后立刻出现。随着程序的运行,当其它部分使用损坏的内存、清理这部分内存或在附近分配其它内存时才会出现问题。
这个程序运行后,会弹出一些内存非法访问的对话框。所以,让我们来应用调试器看看到底发生了什么。
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: 0040000000404000   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>
调试运行时,我们首先会来到第一个断点,当键入“g”后程序会继续运行。然后会出现第二个断点,这次是出现了陷阱。还记得第一篇指南中的内容吗,我们首先来找到为什么这块内存被引用了,是谁引用了它,它是从哪来的。怎么来实现呢?当然是调用堆栈:
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被当作一个指针来用,象前面文章所提到的,[ecx]等价于C语言的DWORD *pECX; *pECX = EAX;所以要看一下ECX来自于哪里。可以看到“ECX,[ESI+0Ch]”,含义如下:
DWORD *pECX, *pESI; pECX = pESI[12/4]; 
请记住,在汇编语言中没有类型一说,所以数组总是以字节为单位而不是象C中以数据类型为单位。所以看一下[ESI+C]的内容,如下所示:
0:000> dc esi + c
00142ac73616664 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。大多数时候,这种问题回被忽视掉,因为是以8为粒度分配内存的,它不会引起问题。但有时候它却是象梦魇一样。因为内存被覆盖掉时不会立即出现问题,一旦问题出现,元凶已经无影无踪了。
断点是个很好的工具。“ba r1 xxxx”表示当有人试图读写这块内存时就中断到这里。当地址是个常量时,这样是很有效的。还有其它方法也是有效的,比如,减少导致问题的函数的功能、使能全局标志等。
在这部分开始时我提到,检测内存泄漏的方法用于检测内存破坏是失效的。这个说法也不完全正确。我只是想说,使用它当前的状态是检测不了内存破坏的。应该做一些改变,比如在内存的开始和结束位置写入一个特定的值,这个值是事先约定好不希望程序改变的。当代码运行后,如果这个值被改变了就有可能出现了内存越界。这个方法的前提是,内存破坏会一直继续越过了内存边界,而且问题只存在于分配的内存中而不是代码的其它地方。
其它工具
下面是一些可以帮助我们检测内存泄漏和破坏的工具。
Performance Monitor
这个工具是Windows perfomon 类工具之一。这个工具允许我们使用特定的选项来查看系统的运行状况。它可以帮你了解进程在内存中整个的运行状况。对于慢速的内存泄漏来说,这是比任务管理器更好的工具。
Bounds Checker
我在上面就已经提到这个公决了。当程序关闭时它会提醒你哪些内存没有被释放。它也可以找到内存破坏的问题。它是一个很简单的工具,当出现内存泄漏时它会显示哪段代码没有释放内存,以及有多少内存没有被释放。
Global Flags
注册表中的一些项也可以用以内存检查。这是调试器的一个功能,注册表中的“gflags”可以用来设置这个功能。在后面的文章中我会讲到这方面的内容。
QuickView: System Explorer
这是我自己在Windows2000及以上系统上写的一个工具。它不会实时显示跟踪数据,但它会反馈出系统个方面的信息来帮助你找到问题所在。
总结
这篇指南介绍了用户模式下的内存泄漏和堆破坏。这片指南还象大家介绍了怎样在自己的程序中跟踪这类问题。
 
 
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值