调试教程第三部分:堆






4.98/5 (94投票s)
2004年3月21日
18分钟阅读

343298

4126
堆简介。
引言
在本系列的上一期调试教程中,我们学习了堆栈。堆栈是用于存储局部变量、参数、返回地址以及编译器想要使用的任何内容的临时存储位置。在本期调试系列中,我们将学习用户模式下的堆。
什么是堆?
堆是进程空间内的一块内存,可用于在应用程序请求时为其提供内存。系统 API 通常会分配一块内存,并根据应用程序的请求提供给应用程序。API 会在每个请求的内存位置添加一个标题,帮助系统将其标记为已使用及其大小。这有助于系统在释放内存时进行处理。(全局变量也位于堆上)。
我上面提到的方法完全在用户模式下进行。这块内存已经分配到您的进程空间中,供您的应用程序使用。这块内存会在特定库启动时分配,例如在 DllMain()
的 MSVCRT.DLL 中。处理 malloc()
的代码通常会利用这块已分配的内存。
可以使用 HeapCreate
等函数将内存堆分配到您的进程中。此函数分配一个堆段并返回段号。然后,您的应用程序可以将此段传递给 HeapAlloc
,从该堆分配内存。这与我描述的 malloc
的工作方式相同,Heap*
函数会管理您分配的段上的所有内存。实际上,这就是 malloc()
内部使用的机制。会为 malloc()
函数调用专门分配一个段。
还有一个名为 VirtualAlloc
的函数。此函数会大规模分配内存供应用程序使用,并提交页面到内存。此函数不使用像 HeapAlloc
那样的预分配堆(您从 HeapCreate
提供的堆),甚至可以指定内存位置,但这不是必需的。这是一种更高级的分配方法,在大多数应用程序中通常不使用也不需要。
这是您不能使用与分配时不同的函数对来释放内存的主要原因之一(即,使用 LocalAlloc()
分配,然后使用 C 的 free()
释放)。这是因为它们可能使用了不同的方法或不同的底层堆进行分配。底层代码通常很“傻”,通常仍会尝试释放它,从而导致内存损坏。这也是为什么任何分配内存的模块都应始终释放它的原因。如果您在一个 DLL 中分配内存,而在另一个 DLL 中释放,会怎样?有时可能有效,但也许有一天,您会扔掉一个调试 DLL 来替换其中一个。我保证您会遇到陷阱,因为调试堆分配包含的信息与发布版本分配的信息不同!这是一个坏习惯!使用匹配的函数对在同一个模块中分配和释放!
分配的内存不是零?
我在上一篇教程中提到过,您可能会在堆栈中看到一些无意义的值,例如函数调用的返回值,而这些调用尚未发生等等。堆栈通常初始化为零,但在使用过程中会变脏。您知道局部变量并非总是初始化,因此如果您进行堆栈调用,这些值在堆栈向上移动时不会重置为零。如果您弹出堆栈中的一个值,堆栈可能会向下移动,但值会保留,除非它们被物理清理。有时堆栈会优化事物,并且不会很好地清理变量。因此,在堆栈中看到“幽灵”值非常常见。堆也可能发生同样的情况。
释放堆上的一个位置不会将其值清除为零,除非您在释放内存之前已经执行了此操作。因此,在分配内存后,您的指针将指向“垃圾”是非常常见的。这通常就是为什么您通常会在分配内存后将其初始化为零的原因。
在释放内存之前“清零内存”可能看起来很奇怪,但如果您的应用程序包含密码等敏感信息,最好在返回之前将堆栈变量也清零。这有助于在敏感信息使用后将其擦除。您不希望程序出现陷阱,而用户的密码就在堆栈或堆上!
堆问题
在本文中,我将介绍堆的两个最常见问题。“内存泄漏”和“堆损坏”。
内存泄漏
如果您注意到随着时间的推移,使用任务管理器时,您的程序内存占用空间会逐渐增大,您可能会意识到存在内存泄漏。内存泄漏基本上意味着您分配了内存但未释放。当程序“忘记”或将分配的内存丢弃到无法再引用或甚至完全不记得它存在的地方时,就会发生“泄漏”。这时我们就开始了实际的“泄漏”。进程的内存占用空间可能会在没有泄漏的情况下增长,程序可能只是需要大量内存。那么,我们从哪里开始呢?
我首先要做的是检查任务管理器。如果泄漏很快,内存可能会快速增加。如果是缓慢泄漏,内存可能会随着时间的推移逐渐增加。首先,您需要知道如何读取任务管理器。
- 虚拟内存 - 此字段表示此进程独有的内存量。如果进程被分页到磁盘,这将是它在分页文件中占用的内存量。
- 内存使用 - 这是“工作集”,即物理内存中的内存量。有些人会混淆,当他们看到这个数字大于虚拟内存大小时。这是因为这个数字不仅包括此进程“独有”的内存,还包括其他进程共享的内存。例如,许多进程使用 kernel32.dll,为每个进程复制相同的代码将是浪费。
因此,一旦确定了泄漏,就需要找到问题。堆问题是最难追踪的问题之一。一个有帮助的方面是,如果存在泄漏,它们很可能来自同一个源。即使有多个源,它们都会泄漏大量数据。这意味着通常情况下,存储的数据都会是相似的。
如果存储的数据相似,您只需要找到大量分配的内存并进行检查。要找到数据,这里有一些技巧:
- 通常,从同一位置分配的内存“通常”大小相同。对于固定结构来说是这样,所以您需要找到数量多且大小相似的分配。但这并不总是如此,例如,基于不同大小的动态分配的字符串。
- 从同一位置分配的内存通常会包含相同或相似的信息或信息类型。这意味着一旦找到内存,就要检查它。比较不同的内存位置,看看它们是否相似。如果您了解数据结构等,可以验证大小并将内存中的数据进行匹配,以尝试对内存的实际内容做出有根据的“猜测”。然后,这可以帮助缩小在代码中查找此类内存分配的范围。
我编写了一个导致内存泄漏的小程序。这个程序非常简单,我们将通过一个简单的场景进行讲解。这将帮助您熟悉堆。
现在,当我们在 Windows 中看到程序出现内存泄漏时,并不一定意味着它来自您的应用程序。如果您使用第三方库或 DLL,也许是它们在泄漏内存。也许您看到内存泄漏,但您并未直接“分配”内存,那么您所做的事情可能间接分配了内存。例如,假设您想打开一个注册表项,您使用了 RegOpenKey
。您知道它是如何实现的吗?不知道。所以,如果您泄漏了“句柄”,是否会有与之相关的内存?可能会有。“句柄泄漏”是另一个我将在后续教程中讨论的问题。
现在,我不是说泄漏注册表项会导致内存增长,也不是说在另一个模块中泄漏内存会通过句柄泄漏显示。我只是说,操作和与外部模块交互可能会间接导致您仍需负责的泄漏。如果您使用的库指定您必须释放或调用某个销毁函数,您应该遵循说明!
因此,我们已经运行了程序,并注意到内存有所增加。让我们看看能否找到它在哪里。首先,我们找到进程的 PID,然后我们可以使用“CDB -P <PID>”。您也可以使用 WinDbg 的 GUI 并从列表中选择进程。一旦我们中断了程序,要做的第一件事就是执行“!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>
如果我转储其中一个地址,就会看到这个:
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。第二个 DWORD
是标志,其余的是分配给程序的内存。
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”字节。这是因为分配大小包括 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; }
您只需在所有分配的末尾附加您自己的头部。这些信息可以很复杂,例如返回分配内存的返回地址、线程标识符等。这就是 boundschecker 之类的程序的工作方式。它们可以简单地替换您的分配,找出谁在分配内存并进行跟踪。您的程序也可以自己做到这一点。您甚至可以创建一个全局变量,并将所有内存添加到一个链表中,然后创建一个调试辅助扩展来遍历它,转储所有内存和信息。创建调试辅助扩展将在后续教程中介绍。
正如您所看到的,您可以简单地创建自己的内存分配挂钩,以在分配的块中添加有用的信息,以帮助您跟踪内存并查找泄漏甚至内存损坏。您甚至可以 #define
此函数,使其在特定模式(如调试)下仅为您自己的代码。在正常构建时,您使用的函数可以被重定义为 LocalAlloc
或 malloc
。您甚至可以始终使用它,并使用特殊的注册表项或标志来启用它。可能性很多。
另外,请记住,您现在需要创建一个“释放”例程。此例程应在调用 free
API 之前简单地减去您的结构大小。
堆损坏
也称为“内存损坏”,基本上是指变量开始覆盖其边界。最简单的堆损坏的常见原因是在释放内存时使用了错误的方法。例如,如果您使用 malloc()
分配,但使用 LocalFree()
释放。它们使用不同的堆,甚至可能使用不同的底层方法来分配或跟踪内存。问题在于,这些函数可能仍会尝试使用它们的算法 free
内存,而不会对要释放的堆进行验证。此时就会发生损坏。请看上面关于创建自己的 malloc()
挂钩函数的章节。其他 API 可能正在执行它们自己的挂钩,因此最好使用匹配的 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: 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>;
我们收到一个首次机会异常。这通常意味着如果我们按“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];
请记住,在汇编中没有类型,因此数组始终以字节大小而不是数据类型大小进行索引。(我假设如果您阅读了本文,您就了解指针!)所以,让我们转储 [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
留出 1 个字符。很多时候,这是可以的,因为我们看到我们以 8 的粒度进行分配,所以它未被注意到。但有时,这是一个噩梦般的查找过程,因为一旦内存被覆盖,当其效果真正显现时,罪魁祸首 **早已消失**!
另一个好方法是逐步执行程序。如果正在被覆盖的位置没有改变,那很简单。逐步执行并不断检查它是否被覆盖。您可以跟踪函数来缩小位置范围。这也假设是一个简单的问题,而不是一个复杂的场景。您可能还需要检查其他内存位置等来进行跟踪。
另一个好工具是“断点”。“ba r1 xxxx”表示“当有人尝试读取或写入此地址时中断”。如果地址是恒定的,这也非常有用。一旦有人尝试写入该地址,它就会中断。您还可以尝试其他方法,例如缩小引起问题的函数功能,启用“全局标志”等。
我知道在上面的第一段中,我提到用于检查内存泄漏的代码不能用于检查堆损坏。这是真也是假。我基本上是说,在当前状态下,您不能指望它来帮助查找内存损坏。稍作调整,您**可以**在分配的开头和结尾填充您不希望程序使用的值。然后,在最后,当您释放数据时,检查签名以验证它是否未被更改。如果已更改,则可能是内存被覆盖。这假设您的损坏是连续的或会击中边界而不会跳过它。这也假设问题出在分配的缓冲区中,而不是您代码的其他地方!
其他工具
还有其他工具可以帮助您发现和跟踪内存损坏和泄漏。
性能监视器
这个工具随 Windows 一起提供,“perfmon”。它允许您使用选择的选项监视和记录系统性能。这有助于您随着时间的推移研究进程的内存占用空间。这是发现缓慢泄漏的“任务管理器”的更好替代方案。
Bounds Checker
这是我上面描述的一个工具。它将帮助您跟踪应用程序关闭时未释放的内存。它还将帮助查找其他类型的泄漏,甚至帮助查找内存损坏。这是一个非常易于使用的工具,并将显示导致未释放分配的代码中的位置。它甚至会告诉您有多少内存未被释放。
全局标志
注册表中有关启用堆验证和堆检查的选项。调试工具中有一个名为“gflags”的实用程序,可以帮助您在注册表中设置这些标志。我可能会在将来的教程中介绍它们,不过调试工具附带了文档。
快速预览:系统浏览器
这是我编写的一个实用程序,它是一个通用的 Windows 2000 及更高版本的调试助手。它目前不提供实时数据,仅用于探索系统的不同方面以帮助您跟踪问题。如果您想使用它,随它一起安装了一个 .RTF 文件,其中解释了所有功能。
结论
本教程应该会向您介绍用户模式下的内存泄漏和堆损坏。本教程还应该帮助您了解如何处理自己应用程序中的此类问题。