调试指南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”来显示进程的所有堆。
下面就要检查一下看哪个堆拥有最多的内存。
黑体部分是申请最多内存的段。然后一个一个来看。
为了将列表缩短,我点击了几下“ControlBreak”来跳出列表。我们可以注意到,后面的内存分配都是一样的大小,而且都比较大。列表每一个域的意义如下:
如果将某一个地址dump出来,情形如下:
第一个DWORD值代表分配的大小。但它被分成了两部分。低字代表当前的大小,高字代表以前的大小。所以为了得到真正的大小需要将这个值左移3位,因为内存是以8位粒度分配的,所以42<<3=210或者528。第二个DOWORD是标志位,剩下的就是程序分配的内存。
如果用“dc”来dump的话,就可以看到,申请的内存中包含了“hi”。如果继续dump的话,每隔528字节就会看到一个“hi”。再让我们看看下一个堆的情况。
在这个堆中,可以看到许多如<address>:e0.e0的内存分配。这就有可能是泄漏。如果用“dc”看一下的话,它们都是一样的:
另外,不用!heap也能达到同样的目的:从堆的首地址开始一直使用“dc”,来寻找相同的字符串。
01c<<3=e0或者224。下面可以看一下源代码:
非常简单的代码,而且显然它会导致快速的内存泄漏。请注意一下:实际内存分配是224字节而不是200字节。这是因为分配的尺寸中包括了2DWORD的头,它也是8字节边界的。
如果将返回给程序的指针减去8,就能得到头信息。从中取出分配的尺寸左移3位然后与这个地址相加就能得到下一个内存分配地址。那上一个为例:
在这里我不会详细介绍标志位,因为我从来没有亲自使用过它。这里面最重要的标志是告诉你当前内存是否被分配了。也就是使用!heap<heap>-a时所看到的,“busy”说明这块内存被使用了,“free”说明内存是空闲的。上面可以看到当内存被分配时标志位的值是“00180700”而被释放时,它的值是“00180400”。你可以自己做一个试验。有一点需要小心的是,在你释放掉内存观察标志位之前确保不要有其它线程再次申请同一块内存。
私有堆和全局堆
前面我向大家介绍了一个命令!heap,但还有一些问题需要指出来:这个命令不会显示全局堆。如果我们创建的是一个全局堆,它不会显示在!heap之后并且也不遵循同私有堆同样的规则。全局堆不会出现象私有堆那样的问题出现。
但全局堆也会遇到其它内存问题,只是不是泄漏而已。这是我们下面要讨论的问题。关于全局堆将在后面的文章中讨论。
跟踪内存分配
还有另外一种办法来跟踪程序中的内存泄漏:创建自己的内存分配函数。象下面这样:
这里只是在内存分配时加入了自己的头。这个头信息还可以更复杂些,比如,还可以包括返回地址、分配者以及线程ID等。其实boundschecker就是这样做的。它们会替换掉内存分配函数然后用实现内存跟踪。我们也可以自己实现这个功能,我们甚至可以创建一个全局变量然后把所有分配的内存都添加进一个链表中,这样就可以生成一个扩展的调试帮助工具来查看所有已分配的内存的信息。我们将在下一篇指南中涉及到这个问题。
如上所示,可以创建自己的内存分配函数在分配的空间中加入有用的信息来帮助我们跟踪内存泄漏或者是内存破坏。还可以使用#define来规定只有在某种模式下才使用自己的代码,比如debug下。这样,我们用普通的编译方式就可以重定义LocalAlloc或者malloc。如果想在任何情况下都一直使用自己的函数可以通过修改注册表的方法来实现。总之,一切皆有可能。
一定要记得创建自己的free函数。这个函数首先减掉自定义的结构的大小,然后再调用freeAPI。
堆破坏
当变量写入超过内存边界时就会出现堆破坏或者叫内存破坏。当使用错误的方法来释放内存时就很有可能出现这种情况。比如,如果你使用malloc()分配了一段内存,然后使用LocalFree()进行释放。因为它们使用不同的堆,也可能使用了不同的实现方法来分配和跟踪内存。问题出自这些函数试图用它们原来的算法来释放内存而不会去做任何检查。就那上面的代码为例,我们使用了自己的malloc方法来分配内存,其它API也会有同样的问题,所以一定要使用与分配内存函数对应的释放函数来释放内存。
另外一个常见的问题就是使用内存时超过了边界。比如,写入的空间尺寸大于分配的尺寸或者随机的向不属于本程序的内存区写入数据。堆破坏问题比泄漏更难追踪到,原来那些跟踪策略不会太奏效。当你重新编译试图用带跟踪信息的分配函数重现问题时,问题可能就不会出现了。这是因为,这个时候更改了内存的尺寸,更改的尺寸可能已经足够使用了,刚才那个问题就不会再发生了。
我这里有一个小程序,它暴露了一些堆的问题。堆问题不会在程序运行后立刻出现。随着程序的运行,当其它部分使用损坏的内存、清理这部分内存或在附近分配其它内存时才会出现问题。
这个程序运行后,会弹出一些内存非法访问的对话框。所以,让我们来应用调试器看看到底发生了什么。
调试运行时,我们首先会来到第一个断点,当键入“g”后程序会继续运行。然后会出现第二个断点,这次是出现了陷阱。还记得第一篇指南中的内容吗,我们首先来找到为什么这块内存被引用了,是谁引用了它,它是从哪来的。怎么来实现呢?当然是调用堆栈:
可以看到,我们在NTDLL中分配了出现问题的内存。再深入一下,看内存会在哪里被引用:
这里好像是一个类似链表的结构。我将我们感兴趣的部分用黑体标识了。首先可以看到,ECX被当作一个指针来用,象前面文章所提到的,[ecx]等价于C语言的DWORD*pECX;*pECX=EAX;所以要看一下ECX来自于哪里。可以看到“ECX,[ESI+0Ch]”,含义如下:
请记住,在汇编语言中没有类型一说,所以数组总是以字节为单位而不是象C中以数据类型为单位。所以看一下[ESI+C]的内容,如下所示:
陷阱语句是:
现在看来就很简单了。因为它指向的是一个字符串。我们只需要在代码中看一下它在哪里被分配的就可以了。
如上所示,我们给字符串分配了5个字节,写入的字符串超越了这个边界。这是一个很简单的例子。有时候,可能只是超出了一个字节,你需要一点一点往回查找看它是字符串的一部分还是字符串的结尾符。我们通常回漏掉最后那个NULL。大多数时候,这种问题回被忽视掉,因为是以8为粒度分配内存的,它不会引起问题。但有时候它却是象梦魇一样。因为内存被覆盖掉时不会立即出现问题,一旦问题出现,元凶已经无影无踪了。
断点是个很好的工具。“bar1xxxx”表示当有人试图读写这块内存时就中断到这里。当地址是个常量时,这样是很有效的。还有其它方法也是有效的,比如,减少导致问题的函数的功能、使能全局标志等。
在这部分开始时我提到,检测内存泄漏的方法用于检测内存破坏是失效的。这个说法也不完全正确。我只是想说,使用它当前的状态是检测不了内存破坏的。应该做一些改变,比如在内存的开始和结束位置写入一个特定的值,这个值是事先约定好不希望程序改变的。当代码运行后,如果这个值被改变了就有可能出现了内存越界。这个方法的前提是,内存破坏会一直继续越过了内存边界,而且问题只存在于分配的内存中而不是代码的其它地方。
其它工具
下面是一些可以帮助我们检测内存泄漏和破坏的工具。
PerformanceMonitor
这个工具是Windowsperfomon类工具之一。这个工具允许我们使用特定的选项来查看系统的运行状况。它可以帮你了解进程在内存中整个的运行状况。对于慢速的内存泄漏来说,这是比任务管理器更好的工具。
BoundsChecker
我在上面就已经提到这个公决了。当程序关闭时它会提醒你哪些内存没有被释放。它也可以找到内存破坏的问题。它是一个很简单的工具,当出现内存泄漏时它会显示哪段代码没有释放内存,以及有多少内存没有被释放。
GlobalFlags
注册表中的一些项也可以用以内存检查。这是调试器的一个功能,注册表中的“gflags”可以用来设置这个功能。在后面的文章中我会讲到这方面的内容。
QuickView:SystemExplorer
这是我自己在Windows2000及以上系统上写的一个工具。它不会实时显示跟踪数据,但它会反馈出系统个方面的信息来帮助你找到问题所在。
总结
这篇指南介绍了用户模式下的内存泄漏和堆破坏。这片指南还象大家介绍了怎样在自己的程序中跟踪这类问题。
在前一部分中我们了解了关于堆栈的知识。堆栈是为本地变量、参数、返回值准备的临时存储区。这一部分里,我们将了解一下堆在用户模式下是怎样一个东西。
什么是堆?
堆是进程空间内的一块存储区,应用程序在需要时可以使用系统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及以上系统上写的一个工具。它不会实时显示跟踪数据,但它会反馈出系统个方面的信息来帮助你找到问题所在。
总结
这篇指南介绍了用户模式下的内存泄漏和堆破坏。这片指南还象大家介绍了怎样在自己的程序中跟踪这类问题。
相关文章推荐
- 一步步教你学会iOS真机调试,常见证书问题的解决方案以及Xcode7之后免$99真机调试
- GDB多线程调试基本命令和实现简介以及一个常见问题的解决
- NTB调试常见问题指南
- LCD调试中的常见问题以及注意事项
- LCD调试中的常见问题以及注意事项
- GDB多线程调试基本命令和实现简介以及一个常见问题的解决
- 理解Tomcat的Classpath-常见问题以及如何解决
- Android Eclipse工程开发中的常见调试问题(一)
- 如何配置java环境以及常见的一些问题处理
- 最难面试的公司以及常见面试问题
- 驱动调试常见问题_Camera
- charles抓包的安装,使用说明以及常见问题解决(windows)
- Xcode8以及iOS10适配等常见问题汇总(整理篇)
- Net 下安装、调试的常见问题与错误!!!
- LCD驱动调试常见问题和一些解决方式
- chrome调试问题几个常见问题
- 使用Sublime Text 3 编译并运行Java程序以及常见的问题
- tomcat下调试jsp的常见问题(一)
- 微信公众号之:JSSDK接入以及invalid signature等常见错误问题
- STM32调试过程中常见的问题及解决方法