您的位置:首页 > 其它

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

2008-01-07 10:54 330 查看
简介
在前一部分中我们了解了关于堆栈的知识。堆栈是为本地变量、参数、返回值准备的临时存储区。这一部分里,我们将了解一下堆在用户模式下是怎样一个东西。
什么是堆?
堆是进程空间内的一块存储区,应用程序在需要时可以使用系统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.VirtualMemory-这个域显示了进程独占的内存量。如果进程将被交换到磁盘,它就显示了在页面文件中占据的存储空间。
2.MemoryUsage-这个域显示了进程使用了多少物理内存。有时候它会比VirtualMemory大,这是因为它不仅包含进程独占的内存汉包含与其它进程共享的内存。比如,许多进程使用kernael32.dll,而将同一段执行代码复制到每个进程空间是很浪费的。
当确定程序中有内存泄漏后,就需要找到问题的所在。而堆中出现的问题是最难跟踪到的。这里有一个窍门:如果是内存泄漏,这些内存分配通常来自统一个地方,即便有很多地方在分配内存,它们也会不停的分配很多内存。也就是说,通常存储的数据是相似的。
这样你置需要找到分配大量内存的地方,检查这些地方。这里还有些技巧:
1.通常来说,出自同一个地方的内存分配会有相同的大小。对于固定大小的结构这样是不错的,所以只需找到分配了许多同样内存的地方。但对于动态分配的内存就不是这样了。
2.出自同一个地方的内存通常有相同或近似的类型或信息。也就是说,比较不同地方的内存分配看它们是否有相似的地方。如果你了解程序的数据结构你就能猜到内存分配出自哪里。这可以帮助你缩小搜索范围。
我这里有一个小程序来模拟内存泄漏。下面我们将会跟踪一下这个泄漏,来帮助你熟悉堆。
其实,当我们从任务管理器中看到我们的程序发生内存泄漏时,这泄漏并不一定出自我们的代码。如果程序中使用了第三方的DLL,泄漏也许是发生在DLL中。如果你看到程序出现了内存泄漏但你并没有直接的分配内存,那有可能你在间接的分配内存。比如,当使用RegOpenKey打开一个注册表项时,你知道它内部是怎么实现的吗?肯定不能。所以只能漏掉这个“Handle”。而这里面是否会有内存分配呢?也许会有。关于“Handle”泄漏的问题我们会在后面的指南中见到。
我并不是说漏掉一个注册表项会导致内存增长,也并不是说,其它模块中出现的内存泄漏会以句柄泄漏的形式出现。我只是想说,与其它模块交互时可能会间接的分配内存,而这些内存也必须由你来负责释放。如果有些库明确说明需要调用free或其它销毁函数,我们必须遵守这个说明。
现在来运行我们的小程序,我们会发现内存增长了。接下来我们看看能不能找到它在哪发生的。首先找到进程的PID,然后运行“CDB–P<PID>”。当然,也可以用WinDbg,从UI中选择这个进程。进入以后,执行“!heap”来显示进程的所有堆。
0:000>!heap

NtGlobalFlagenablesfollowingdebuggingaidsfornewheaps:tailchecking

disablecoalescingoffreeblocks

IndexAddressNameDebuggingoptionsenabled

1:00140000tailcheckingfreecheckingvalidateparameters

2:00240000tailcheckingfreecheckingvalidateparameters

3:00250000tailcheckingfreecheckingvalidateparameters

4:00320000tailcheckingfreecheckingvalidateparameters

0:000>

下面就要检查一下看哪个堆拥有最多的内存。
0:000>!heap00140000

IndexAddressNameDebuggingoptionsenabled

1:00140000

Segmentat00140000to00240000(00100000bytescommitted)

Segmentat00510000to00610000(00100000bytescommitted)

Segmentat00610000to00810000(00051000bytescommitted)

2:00240000

3:00250000

4:00320000

0:000>!heap00240000

IndexAddressNameDebuggingoptionsenabled

1:00140000

2:00240000

Segmentat00240000to00250000(00006000bytescommitted)

3:00250000

4:00320000

0:000>!heap00250000

IndexAddressNameDebuggingoptionsenabled

1:00140000

2:00240000

3:00250000

Segmentat00250000to00260000(00001000bytescommitted)

4:00320000

0:000>!heap00320000

IndexAddressNameDebuggingoptionsenabled

1:00140000

2:00240000

3:00250000

4:00320000

Segmentat00320000to00330000(00010000bytescommitted)

Segmentat00410000to00510000(000ee000bytescommitted)

0:000>

黑体部分是申请最多内存的段。然后一个一个来看。
0:000>!heap00140000-a

IndexAddressNameDebuggingoptionsenabled

1:00140000

Segmentat00140000to00240000(00100000bytescommitted)

Segmentat00510000to00610000(00100000bytescommitted)

Segmentat00610000to00810000(00051000bytescommitted)

Flags:50000062

ForceFlags:40000060

Granularity:8bytes

SegmentReserve:00400000

SegmentCommit:00002000

DeCommitBlockThres:00000200

DeCommitTotalThres:00002000

TotalFreeSize:00000226

Max.AllocationSize:7ffdefff

LockVariableat:00140608

NextTagIndex:0000

MaximumTagIndex:0000

TagEntries:00000000

PsuedoTagEntries:00000000

VirtualAllocList:00140050

UCRFreeList:00140598

FreeListUsage:00040000004000000000000000000000

FreeList[00]at00140178:00660118.00660118

Unabletoreadnt!_HEAP_FREE_ENTRYstructureat00660118

FreeList[12]at00140208:0023ff78.0023ff78

Unabletoreadnt!_HEAP_FREE_ENTRYstructureat0023ff78

FreeList[36]at00140328:0060fe58.0060fe58

Unabletoreadnt!_HEAP_FREE_ENTRYstructureat0060fe58

Segment00at00140640:

Flags:00000000

Base:00140000

FirstEntry:00140680

LastEntry:00240000

TotalPages:00000100

TotalUnCommit:00000000

LargestUnCommit:00000000

UnCommittedRanges:(0)


HeapentriesforSegment00inHeap00140000

00140000:00000.00640[01]-busy(640)

00140640:00640.00040[01]-busy(40)

00140680:00040.01818[07]-busy(1800),

tailfill-unabletoreadheapentryextraat00141e90

00141e98:01818.00040[07]-busy(22),

tailfill-unabletoreadheapentryextraat00141ed0

00141ed8:00040.00020[07]-busy(5),

tailfill-unabletoreadheapentryextraat00141ef0

00141ef8:00020.002f0[07]-busy(2d8),

tailfill-unabletoreadheapentryextraat001421e0

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

tailfill-unabletoreadheapentryextraat00142510

00142518:00330.00330[07]-busy(314),

tailfill-unabletoreadheapentryextraat00142840

00142848:00330.00040[07]-busy(24),

tailfill-unabletoreadheapentryextraat00142880

00142888:00040.00040[07]-busy(24),

tailfill-unabletoreadheapentryextraat001428c0

001428c8:00040.00028[07]-busy(10),

tailfill-unabletoreadheapentryextraat001428e8

001428f0:00028.00058[07]-busy(40),

tailfill-unabletoreadheapentryextraat00142940

00142948:00058.00058[07]-busy(40),

tailfill-unabletoreadheapentryextraat00142998

001429a0:00058.00060[07]-busy(44),

tailfill-unabletoreadheapentryextraat001429f8

00142a00:00060.00020[07]-busy(1),

tailfill-unabletoreadheapentryextraat00142a18

00142a20:00020.00028[07]-busy(10),

tailfill-unabletoreadheapentryextraat00142a40

00142a48:00028.00050[07]-busy(36),

tailfill-unabletoreadheapentryextraat00142a90

00142a98:00050.00210[07]-busy(1f4),

tailfill-unabletoreadheapentryextraat00142ca0

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

tailfill-unabletoreadheapentryextraat00142eb0

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

tailfill-unabletoreadheapentryextraat001430c0

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

tailfill-unabletoreadheapentryextraat001432d0

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

tailfill-unabletoreadheapentryextraat001434e0

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

tailfill-unabletoreadheapentryextraat001436f0

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

tailfill-unabletoreadheapentryextraat00143900

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

tailfill-unabletoreadheapentryextraat00143b10

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

tailfill-unabletoreadheapentryextraat00143d20

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

tailfill-unabletoreadheapentryextraat00143f30

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

tailfill-unabletoreadheapentryextraat00144140

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

tailfill-unabletoreadheapentryextraat00144350

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

tailfill-unabletoreadheapentryextraat00144560

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

tailfill-unabletoreadheapentryextraat00144770

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

tailfill-unabletoreadheapentryextraat00144980

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

tailfill-unabletoreadheapentryextraat00144b90

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

tailfill-unabletoreadheapentryextraat00144da0

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

tailfill-unabletoreadheapentryextraat00144fb0

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

tailfill-unabletoreadheapentryextraat001451c0

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

tailfill-unabletoreadheapentryextraat001453d0

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

tailfill-unabletoreadheapentryextraat001455e0

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

tailfill-unabletoreadheapentryextraat001457f0

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

tailfill-unabletoreadheapentryextraat00145a00

为了将列表缩短,我点击了几下“ControlBreak”来跳出列表。我们可以注意到,后面的内存分配都是一样的大小,而且都比较大。列表每一个域的意义如下:
<ADDRESS>:<CurrentSize>.<PREVIOUSSize>

如果将某一个地址dump出来,情形如下:
0:000>dd001457f8

001457f800420042001c07000000696800000000

0014580800000000000000000000000000000000

0014581800000000000000000000000000000000

0014582800000000000000000000000000000000

0014583800000000000000000000000000000000

0014584800000000000000000000000000000000

0014585800000000000000000000000000000000

0014586800000000000000000000000000000000

第一个DWORD值代表分配的大小。但它被分成了两部分。低字代表当前的大小,高字代表以前的大小。所以为了得到真正的大小需要将这个值左移3位,因为内存是以8位粒度分配的,所以42<<3=210或者528。第二个DOWORD是标志位,剩下的就是程序分配的内存。
0:000>dc001457f8

001457f800420042001c07000000696800000000B.B.....hi......

0014580800000000000000000000000000000000................

0014581800000000000000000000000000000000................

0014582800000000000000000000000000000000................

0014583800000000000000000000000000000000................

0014584800000000000000000000000000000000................

0014585800000000000000000000000000000000................

0014586800000000000000000000000000000000................

0:000>

如果用“dc”来dump的话,就可以看到,申请的内存中包含了“hi”。如果继续dump的话,每隔528字节就会看到一个“hi”。再让我们看看下一个堆的情况。
0:000>!heap

NtGlobalFlagenablesfollowingdebuggingaidsfornewheaps:tailchecking

disablecoalescingoffreeblocks

IndexAddressNameDebuggingoptionsenabled

1:00140000tailcheckingfreecheckingvalidateparameters

2:00240000tailcheckingfreecheckingvalidateparameters

3:00250000tailcheckingfreecheckingvalidateparameters

4:00320000tailcheckingfreecheckingvalidateparameters

0:000>!heap00320000-a

IndexAddressNameDebuggingoptionsenabled

1:00140000

2:00240000

3:00250000

4:00320000

Segmentat00320000to00330000(00010000bytescommitted)

Segmentat00410000to00510000(000ee000bytescommitted)

Flags:50001062

ForceFlags:40000060

Granularity:8bytes

SegmentReserve:00200000

SegmentCommit:00002000

DeCommitBlockThres:00000200

DeCommitTotalThres:00002000

TotalFreeSize:000000b3

Max.AllocationSize:7ffdefff

LockVariableat:00320608

NextTagIndex:0000

MaximumTagIndex:0000

TagEntries:00000000

PsuedoTagEntries:00000000

VirtualAllocList:00320050

UCRFreeList:00320598

FreeListUsage:00000800000000000000000000000000

FreeList[00]at00320178:004fdac8.004fdac8

Unabletoreadnt!_HEAP_FREE_ENTRYstructureat004fdac8

FreeList[0b]at003201d0:0032ffb0.0032ffb0

Unabletoreadnt!_HEAP_FREE_ENTRYstructureat0032ffb0

Segment00at00320640:

Flags:00000000

Base:00320000

FirstEntry:00320680

LastEntry:00330000

TotalPages:00000010

TotalUnCommit:00000000

LargestUnCommit:00000000

UnCommittedRanges:(0)


HeapentriesforSegment00inHeap00320000

00320000:00000.00640[01]-busy(640)

00320640:00640.00040[01]-busy(40)

00320680:00040.01818[07]-busy(1800),

tailfill-unabletoreadheapentryextraat00321e90

00321e98:01818.000a0[07]-busy(88),

tailfill-unabletoreadheapentryextraat00321f30

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

tailfill-unabletoreadheapentryextraat003223c8

003223d0:00498.00098[07]-busy(80),

tailfill-unabletoreadheapentryextraat00322460

00322468:00098.00028[07]-busy(d),

tailfill-unabletoreadheapentryextraat00322488

00322490:00028.000e0[07]-busy(c8),

tailfill-unabletoreadheapentryextraat00322568

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

tailfill-unabletoreadheapentryextraat00322648

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

tailfill-unabletoreadheapentryextraat00322728

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

tailfill-unabletoreadheapentryextraat00322808

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

tailfill-unabletoreadheapentryextraat003228e8

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

tailfill-unabletoreadheapentryextraat003229c8

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

tailfill-unabletoreadheapentryextraat00322ab0

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

tailfill-unabletoreadheapentryextraat00322ce8

00322cf0:00238.000a8[07]-busy(90),

tailfill-unabletoreadheapentryextraat00322d90

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

tailfill-unabletoreadheapentryextraat00322de8

00322df0:00058.00060[07]-busy(41),

tailfill-unabletoreadheapentryextraat00322e48

00322e50:00060.00050[07]-busy(31),

tailfill-unabletoreadheapentryextraat00322e98

00322ea0:00050.00038[07]-busy(1b),

tailfill-unabletoreadheapentryextraat00322ed0

00322ed8:00038.00040[07]-busy(26),

tailfill-unabletoreadheapentryextraat00322f10

00322f18:00040.00030[07]-busy(11),

tailfill-unabletoreadheapentryextraat00322f40

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

tailfill-unabletoreadheapentryextraat00322f70

00322f78:00030.00028[07]-busy(d),

tailfill-unabletoreadheapentryextraat00322f98

00322fa0:00028.00048[07]-busy(2f),

tailfill-unabletoreadheapentryextraat00322fe0

00322fe8:00048.000d0[07]-busy(b1),

tailfill-unabletoreadheapentryextraat003230b0

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

tailfill-unabletoreadheapentryextraat00323130

00323138:00080.00038[07]-busy(1c),

tailfill-unabletoreadheapentryextraat00323168

00323170:00038.00048[07]-busy(2d),

tailfill-unabletoreadheapentryextraat003231b0

003231b8:00048.00040[07]-busy(22),

tailfill-unabletoreadheapentryextraat003231f0

003231f8:00040.00030[07]-busy(17),

tailfill-unabletoreadheapentryextraat00323220

00323228:00030.00028[07]-busy(e),

tailfill-unabletoreadheapentryextraat00323248

00323250:00028.00168[07]-busy(149),

tailfill-unabletoreadheapentryextraat003233b0

003233b8:00168.00058[07]-busy(39),

tailfill-unabletoreadheapentryextraat00323408

00323410:00058.00038[07]-busy(1b),

tailfill-unabletoreadheapentryextraat00323440

00323448:00038.00060[07]-busy(43),

tailfill-unabletoreadheapentryextraat003234a0

003234a8:00060.00030[07]-busy(12),

tailfill-unabletoreadheapentryextraat003234d0

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

tailfill-unabletoreadheapentryextraat00323500

00323508:00030.00038[07]-busy(1e),

tailfill-unabletoreadheapentryextraat00323538

00323540:00038.00028[07]-busy(c),

tailfill-unabletoreadheapentryextraat00323560

00323568:00028.00030[07]-busy(14),

tailfill-unabletoreadheapentryextraat00323590

00323598:00030.00028[07]-busy(f),

tailfill-unabletoreadheapentryextraat003235b8

003235c0:00028.00030[07]-busy(18),

tailfill-unabletoreadheapentryextraat003235e8

003235f0:00030.00040[07]-busy(28),

tailfill-unabletoreadheapentryextraat00323628

00323630:00040.00040[07]-busy(27),

tailfill-unabletoreadheapentryextraat00323668

00323670:00040.00038[07]-busy(19),

tailfill-unabletoreadheapentryextraat003236a0

003236a8:00038.00030[07]-busy(17),

tailfill-unabletoreadheapentryextraat003236d0

003236d8:00030.00050[07]-busy(34),

tailfill-unabletoreadheapentryextraat00323720

00323728:00050.00030[07]-busy(11),

tailfill-unabletoreadheapentryextraat00323750

00323758:00030.00030[07]-busy(14),

tailfill-unabletoreadheapentryextraat00323780

00323788:00030.00068[07]-busy(4a),

tailfill-unabletoreadheapentryextraat003237e8

003237f0:00068.00818[07]-busy(800),

tailfill-unabletoreadheapentryextraat00324000

00324008:00818.000e0[07]-busy(c8),

tailfill-unabletoreadheapentryextraat003240e0

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

tailfill-unabletoreadheapentryextraat003241c0

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

tailfill-unabletoreadheapentryextraat003242a0

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

tailfill-unabletoreadheapentryextraat00324380

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

tailfill-unabletoreadheapentryextraat00324460

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

tailfill-unabletoreadheapentryextraat00324540

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

tailfill-unabletoreadheapentryextraat00324620

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

tailfill-unabletoreadheapentryextraat00324700

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

tailfill-unabletoreadheapentryextraat003247e0

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

tailfill-unabletoreadheapentryextraat003248c0

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

tailfill-unabletoreadheapentryextraat003249a0

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

tailfill-unabletoreadheapentryextraat00324a80

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

tailfill-unabletoreadheapentryextraat00324b60

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

tailfill-unabletoreadheapentryextraat00324c40

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

tailfill-unabletoreadheapentryextraat00324d20

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

tailfill-unabletoreadheapentryextraat00324e00

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

tailfill-unabletoreadheapentryextraat00324ee0

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

tailfill-unabletoreadheapentryextraat00324fc0

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

tailfill-unabletoreadheapentryextraat003250a0

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

tailfill-unabletoreadheapentryextraat00325180

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

tailfill-unabletoreadheapentryextraat00325260

在这个堆中,可以看到许多如<address>:e0.e0的内存分配。这就有可能是泄漏。如果用“dc”看一下的话,它们都是一样的:
0:000>dc00325188

00325188001c001c001807006664736161667361........asdfasfa

0032519873666473736664617366646161666164sdfsadfsadfsdafa

003251a861736673736166646173666473616664sfsadfasdfsadfas

003251b866736166736664736673666166647361fasfsdfsafsfasdf

003251c800617364baadf00dbaadf00dbaadf00ddsa.............

003251d8baadf00dbaadf00dbaadf00dbaadf00d................

003251e8baadf00dbaadf00dbaadf00dbaadf00d................

003251f8baadf00dbaadf00dbaadf00dbaadf00d................

另外,不用!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>dc0325188+e0

00325268001c001c001807006664736161667361........asdfasfa

0032527873666473736664617366646161666164sdfsadfsadfsdafa

0032528861736673736166646173666473616664sfsadfasdfsadfas

0032529866736166736664736673666166647361fasfsdfsafsfasdf

003252a800617364baadf00dbaadf00dbaadf00ddsa.............

003252b8baadf00dbaadf00dbaadf00dbaadf00d................

003252c8baadf00dbaadf00dbaadf00dbaadf00d................

003252d8baadf00dbaadf00dbaadf00dbaadf00d................

0:000>

在这里我不会详细介绍标志位,因为我从来没有亲自使用过它。这里面最重要的标志是告诉你当前内存是否被分配了。也就是使用!heap<heap>-a时所看到的,“busy”说明这块内存被使用了,“free”说明内存是空闲的。上面可以看到当内存被分配时标志位的值是“00180700”而被释放时,它的值是“00180400”。你可以自己做一个试验。有一点需要小心的是,在你释放掉内存观察标志位之前确保不要有其它线程再次申请同一块内存。
私有堆和全局堆
前面我向大家介绍了一个命令!heap,但还有一些问题需要指出来:这个命令不会显示全局堆。如果我们创建的是一个全局堆,它不会显示在!heap之后并且也不遵循同私有堆同样的规则。全局堆不会出现象私有堆那样的问题出现。
但全局堆也会遇到其它内存问题,只是不是泄漏而已。这是我们下面要讨论的问题。关于全局堆将在后面的文章中讨论。
跟踪内存分配
还有另外一种办法来跟踪程序中的内存泄漏:创建自己的内存分配函数。象下面这样:
PVOIDMyAllocationRoutine(DWORDdwSize)

{

PVOIDpMem=malloc(dwSize+sizeof(_DEBUG_STRUCTURE));


if(pMem)

{

_DEBUG_STRUCTURE*pDebugStruc=(_DEBUG_STRUCTURE*)pMem;


/*FillInYourDebugInformationHere*/


/*MakeSureYouGivetheApplicationthememoryAFTERyourdebugstructure*/

pMem=pDebugStruc+1;

}


returnpMem;

}

这里只是在内存分配时加入了自己的头。这个头信息还可以更复杂些,比如,还可以包括返回地址、分配者以及线程ID等。其实boundschecker就是这样做的。它们会替换掉内存分配函数然后用实现内存跟踪。我们也可以自己实现这个功能,我们甚至可以创建一个全局变量然后把所有分配的内存都添加进一个链表中,这样就可以生成一个扩展的调试帮助工具来查看所有已分配的内存的信息。我们将在下一篇指南中涉及到这个问题。
如上所示,可以创建自己的内存分配函数在分配的空间中加入有用的信息来帮助我们跟踪内存泄漏或者是内存破坏。还可以使用#define来规定只有在某种模式下才使用自己的代码,比如debug下。这样,我们用普通的编译方式就可以重定义LocalAlloc或者malloc。如果想在任何情况下都一直使用自己的函数可以通过修改注册表的方法来实现。总之,一切皆有可能。
一定要记得创建自己的free函数。这个函数首先减掉自定义的结构的大小,然后再调用freeAPI。
堆破坏
当变量写入超过内存边界时就会出现堆破坏或者叫内存破坏。当使用错误的方法来释放内存时就很有可能出现这种情况。比如,如果你使用malloc()分配了一段内存,然后使用LocalFree()进行释放。因为它们使用不同的堆,也可能使用了不同的实现方法来分配和跟踪内存。问题出自这些函数试图用它们原来的算法来释放内存而不会去做任何检查。就那上面的代码为例,我们使用了自己的malloc方法来分配内存,其它API也会有同样的问题,所以一定要使用与分配内存函数对应的释放函数来释放内存。
另外一个常见的问题就是使用内存时超过了边界。比如,写入的空间尺寸大于分配的尺寸或者随机的向不属于本程序的内存区写入数据。堆破坏问题比泄漏更难追踪到,原来那些跟踪策略不会太奏效。当你重新编译试图用带跟踪信息的分配函数重现问题时,问题可能就不会出现了。这是因为,这个时候更改了内存的尺寸,更改的尺寸可能已经足够使用了,刚才那个问题就不会再发生了。
我这里有一个小程序,它暴露了一些堆的问题。堆问题不会在程序运行后立刻出现。随着程序的运行,当其它部分使用损坏的内存、清理这部分内存或在附近分配其它内存时才会出现问题。
这个程序运行后,会弹出一些内存非法访问的对话框。所以,让我们来应用调试器看看到底发生了什么。
C:/programs/DirectX/Games/src/Games/temp/bin>cdbtemp


Microsoft(R)WindowsDebuggerVersion6.3.0005.1

Copyright(c)MicrosoftCorporation.Allrightsreserved.


CommandLine:temp

Symbolsearchpathis:SRV*c:/symbols*http://msdl.microsoft.com/download/symbols


Executablesearchpathis:

ModLoad:0040000000404000temp.exe

ModLoad:77f5000077ff7000ntdll.dll

ModLoad:77e6000077f46000C:/WINDOWS.0/system32/kernel32.dll

ModLoad:77c1000077c63000C:/WINDOWS.0/system32/MSVCRT.dll

(a20.710):Breakinstructionexception-code80000003(firstchance)

eax=00241eb4ebx=7ffdf000ecx=00000004edx=77f51310esi=00241eb4edi=00241f48

eip=77f75a58esp=0012fb38ebp=0012fc2ciopl=0nvupeiplnznapenc

cs=001bss=0023ds=0023es=0023fs=003bgs=0000efl=00000202

ntdll!DbgBreakPoint:

77f75a58ccint3

0:000>g

(a20.710):Accessviolation-codec0000005(firstchance)

Firstchanceexceptionsarereportedbeforeanyexceptionhandling.

Thisexceptionmaybeexpectedandhandled.

eax=61736664ebx=00000004ecx=73616664edx=00142ab8esi=00142ab8edi=00140000

eip=77f8452desp=0012f7e4ebp=0012f9fciopl=0nvupeiplzrnaponc

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

ntdll!RtlAllocateHeapSlowly+0x6bd:

77f8452d8901mov[ecx],eaxds:0023:73616664=????????

0:000>

调试运行时,我们首先会来到第一个断点,当键入“g”后程序会继续运行。然后会出现第二个断点,这次是出现了陷阱。还记得第一篇指南中的内容吗,我们首先来找到为什么这块内存被引用了,是谁引用了它,它是从哪来的。怎么来实现呢?当然是调用堆栈:
0:000>kb

ChildEBPRetAddrArgstoChild

0012f9fc77f9d959001400005014016900000006ntdll!RtlAllocateHeapSlowly+0x6bd

0012fa8077f83eb1001400005014016900000006ntdll!RtlDebugAllocateHeap+0xaf

0012fcac77f589f2001400004014006800000006ntdll!RtlAllocateHeapSlowly+0x41

0012fee477e7a6d4001400004014006800000006ntdll!RtlAllocateHeap+0xe44

0012ff3000401024000000400000000600000000kernel32!LocalAlloc+0x58

0012ff4c0040113b000000010032247000322cf8temp!main+0x24

0012ffc077e814c700000000000000007ffdf000temp!mainCRTStartup+0xe3

0012fff000000000004010580000000078746341kernel32!BaseProcessStart+0x23

0:000>

可以看到,我们在NTDLL中分配了出现问题的内存。再深入一下,看内存会在哪里被引用:
0:000>ueip-20

ntdll!RtlAllocateHeapSlowly+0x69d:

77f8450d058845b356addeax,0x56b34588

77f845128b7de4movedi,[ebp-0x1c]

77f8451557pushedi

77f84516e85eeaffffcallntdll!RtlpUpdateIndexRemoveBlock(77f82f79)

77f8451b8b4608moveax,[esi+0x8]

77f8451e89855cffffffmov[ebp-0xa4],eax

77f845248b4e0cmovecx,[esi+0xc]

77f84527898d58ffffffmov[ebp-0xa8],ecx

0:000>u

ntdll!RtlAllocateHeapSlowly+0x6bd:

77f8452d8901mov[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>dcesi+c

00142ac473616664667361667366647366736661dfasfasfsdfsafsf

00142ad46664736100617364feeefeeefeeefeeeasdfdsa.........

00142ae4feeefeeefeeefeeefeeefeeefeeefeee................

00142af4feeefeeefeeefeeefeeefeeefeeefeee................

00142b04feeefeeefeeefeeefeeefeeefeeefeee................

00142b14feeefeeefeeefeeefeeefeeefeeefeee................

00142b24feeefeeefeeefeeefeeefeeefeeefeee................

00142b34feeefeeefeeefeeefeeefeeefeeefeee................

陷阱语句是:
77f8452d8901mov[ecx],eaxds: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为粒度分配内存的,它不会引起问题。但有时候它却是象梦魇一样。因为内存被覆盖掉时不会立即出现问题,一旦问题出现,元凶已经无影无踪了。
断点是个很好的工具。“bar1xxxx”表示当有人试图读写这块内存时就中断到这里。当地址是个常量时,这样是很有效的。还有其它方法也是有效的,比如,减少导致问题的函数的功能、使能全局标志等。
在这部分开始时我提到,检测内存泄漏的方法用于检测内存破坏是失效的。这个说法也不完全正确。我只是想说,使用它当前的状态是检测不了内存破坏的。应该做一些改变,比如在内存的开始和结束位置写入一个特定的值,这个值是事先约定好不希望程序改变的。当代码运行后,如果这个值被改变了就有可能出现了内存越界。这个方法的前提是,内存破坏会一直继续越过了内存边界,而且问题只存在于分配的内存中而不是代码的其它地方。
其它工具
下面是一些可以帮助我们检测内存泄漏和破坏的工具。
PerformanceMonitor
这个工具是Windowsperfomon类工具之一。这个工具允许我们使用特定的选项来查看系统的运行状况。它可以帮你了解进程在内存中整个的运行状况。对于慢速的内存泄漏来说,这是比任务管理器更好的工具。
BoundsChecker
我在上面就已经提到这个公决了。当程序关闭时它会提醒你哪些内存没有被释放。它也可以找到内存破坏的问题。它是一个很简单的工具,当出现内存泄漏时它会显示哪段代码没有释放内存,以及有多少内存没有被释放。
GlobalFlags
注册表中的一些项也可以用以内存检查。这是调试器的一个功能,注册表中的“gflags”可以用来设置这个功能。在后面的文章中我会讲到这方面的内容。
QuickView:SystemExplorer
这是我自己在Windows2000及以上系统上写的一个工具。它不会实时显示跟踪数据,但它会反馈出系统个方面的信息来帮助你找到问题所在。
总结
这篇指南介绍了用户模式下的内存泄漏和堆破坏。这片指南还象大家介绍了怎样在自己的程序中跟踪这类问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: