Windows用户态程序高效排错 Heap和Stack [转帖]

平坦内存空间中的层次结构:Heap和Stack

本小结主要介绍Heap相关的崩溃和内存泄漏,和如何使用pageheap来排错。首先介绍heap的原理,不同层面的内存分配,接下来通过例子代码举例演示heap问题的严重性和欺骗性。最后介绍如何使用pageheap工具高效地对heap问题排错。

2.4.1  Heap是对平坦空间的高效管理和利用

内存是容纳代码和资料的空间。无论是stack,heap还是DLL,都是“生长”在内存上的。代码的执行效果其实是对内存上的资料进行转化。内存是导致问题最多的地方,比如内存不足、内存访问违例、内存泄漏等,都是常见的问题。

关于内存的详细信息,Programming Applications for Microsoft Windows书中有详细介绍。这里针对排错作一些补充。

Windows API中有两类内存分配函数。分别是:VirtualAlloc和HeapAlloc。前一种是向操作系统申请4KB为边界的整块内存,后者是分配任意大小的内存块。区别在于,后者的实现依赖于前者。换句话说,操作系统管理内存的最小单位是4KB,这个粒度是固定的(其实根据芯片是可以做调整的。这里只讨论最普遍的情况)。但用户的资料不可能恰好都是4KB大小。用4KB作单位,难免会产生很多浪费。解决办法是依靠用户态代码的薄计工作,实现比4KB单位更小的分配粒度。换句话说,用户态的程序需要实现一个Memory Manager,通过自身的管理,在4KB为粒度的基础上,提供以字节为粒度的内存分配,释放功能,并且能够平衡好时间利用率和空间利用率。

Windows提供了Heap Manager完成上述功能。HeapAlloc函数是Heap Manager的分配函数。Heap Manager的工作方式大概是这样:首先分配足够的大的4 KB倍数的连续内存空间;然后在这块内存上开辟一小块区域用来做薄计;接下来把这连续的大块内存分割成很多尺寸不等的小块,把每一小块的信息记录到薄计里面。薄计记录了每一小块的起始地址和长度,以及是否已经分配。

当内存请求发生的时候,HeapManager根据请求的长度,在薄计信息里面找到大小最合适的一块空间,把这块空间标记成已经分配,然后把这块空间的地址返回,这样就完成了一次内存分配。如果找不到长度足够大的空闲小块,Heap Manager继续以4 KB为粒度向系统申请更多的内存。

当用户需要释放内存的时候,调用HeapFree,同时传入起始地址。HeapManager在薄计信息中找到这块地址,把这块地址的信息由已经分配改回没有分配。当Heap Manager发现有大量的连续空闲空间的时候,也会调用VirtualFree来把这些内存归还给操作系统。在实现上面这些基本功能的情况下,HeapManager还需要考虑到:

1.         分配的内存最好是在4字节边界上,这样可以提高内存访问效率。

2.         做好线程同步,保证多个线程同时分配内存的时候不会出现错误。

3.         尽可能节省维护薄计的开销,提高性能,避免不必要的计算和检查。所以HeapManager假设用户的代码没有bug,比如用户代码永远不会越界对内存块进行存取,这样就可以省去检查核对的开销。

4.         优化内部的内存块管理。比如灵活地合并连续的内存小块以便满足长尺寸的内存申请,或者拆分连续内存块提高小尺寸的内存使用率。

有了上面的理解后,看下面一些情况:

1.         如果首先用HeapAlloc分配了一块空间,然后用HeapFree释放了这块空间。但是在释放后,继续对这块空间做操作,程序会发生访问违例错误吗?答案是不会,除非HeapManager恰好把那块地址用VirtualFree返还给操作系统了。但是带来的结果是什么?是“非预期结果”。也就是说,谁都无法保证最后会产生什么情况。程序可能不会有什么问题,也可能会格式化整个硬盘。出现得最多的情况是,这块内存后来被Heap Manager重新分配出去。导致两个本应指向不同地址的指针,指向同一个地址。伴随而来的是资料损坏或者访问违例等等。

2.         如果用HeapAlloc分配了100KB的空间,但是访问的长度超过了100KB,会怎么样?如果100KB恰好在4KB内存边界上,而且恰好后面的内存地址并没有被映像上来,程序不会崩溃的。这时,越界的写操作,要么写到别的内存块上,要么就写入薄计信息中,破坏了薄计。导致的结果是HeapManager维护的数据损坏,导致“非预期结果”。

3.         其他错误的代码,比如对同一个地址HeapFree了两次,多线程访问的时候忘记在调用HeapAllocate的第二个参数中传入SERIALIZE bit等等,都会导致“非预期结果”。

总的来说,上面这些情况都会导致非预期结果。如果问题发生后程序立刻崩溃,或者抛出异常,则可以在第一时间截获这个错误。但是,现实的情况是,这些错误不会有及时的效果,错误带来的后果会暂时隐藏起来,在程序继续执行几个小时后,突然在一些看起来绝对不可能出现错误的地方崩溃。比如在调用HeapAllocate/HeapFree的时候崩溃。比如访问一个刚刚分配好的地址的时候崩溃。这个时候哪怕抓到了崩溃的详细信息也无济于事,因为问题根源潜伏在很久以前。这种根源在前现象在后的情况会给调试带来极大的困难。

仔细考虑这种难于调试的情况,错误之所以没有在第一时间暴露,在于下面两点:

1.         Heap每一块内存的界限是Heap Manager定义的,而内存访问无效的界限,是操作系统定义的。哪怕访问越界,如果越界的地方已经有映像上来的4KB为粒度的内存页,程序就不会立刻崩溃。

2.         为了提高效率,Heap Manager不会主动检查自身的数据结构是否被破坏。

所以,为了方便检查Heap上的错误,让现象尽早表现出来,Heap Manager应该这样管理内存:

1.         把所有的Heap内存都分配到4KB页的结尾,然后把下一个4KB页面标记为不可访问。越界访问发生时候,就会访问到无效地址,程序就立刻崩溃。

2.         每次调用Heap相关函数的时候,Heap Manager主动去检查自身的数据结构是否被破坏。如果检查到这样的情况,就主动报告出来。

接下来我们会分析如何使用Pageheap帮忙做到上面两点。如果想了解更多Windows上Heap的知识,以及如何用Windbg中的!heap命令检查Heap,请参考:

Debug Tutorial Part 3: The Heap

http://www.codeproject.com/debug/cdbntsd3.asp

2.4.2  PageHeap,调试Heap问题的工具

幸运的是,Heap Manager的确提供了主动检查错误的功能。只需要在注册表里面做对应的修改,操作系统就会根据设置来改变Heap Manager的行为。Pageheap是用来配置该注册表的工具。关于heap的详细信息和原理请参考:

How to use Pageheap.exe in Windows XP and Windows 2000

http://support.microsoft.com/kb/286470/en-us

Pageheap,Gflag和后面介绍的Application Verifier工具一样,都是方便修改对应注册表的工具。如果不使用这两个工具,直接修改注册表也可以达到一样的效果。3个工具里面Application Verifier是目前的主流,Gflag是老牌。除了heap问题外,这两个工具还可以修改其他的调试选项,后面都有说明。Pageheap.exe工具主要针对heap问题,使用起来简单方便。目前gflag.exe包含在调试器的安装包中,Application Verifier可以单独下载安装。如果调试安装包中没有包含pageheap.exe,可以从这里下载:

http://www.heijoy.com/debugdoc/pageheap.zip

http://blogs.msdn.com/lixiong/attachment/2792912.ashx

简单例子的多种情况

看几个简单的但是却很有意义的例子:

用release模式编译运行下面的代码:

  char *p=(char*)malloc(1024);

  p[1024]=1;

这里往分配的空间多写一个字节。但是在release模式下运行,程序不会崩溃。

假设上面的代码编译成mytest.exe,用下面的方法可以对mytest.exe激活pageheap:

C:\Debuggers\pageheap>pageheap /enable mytest.exe /full

直接运行pageheap可以查看当前pageheap的激活状态:

C:\Debuggers\pageheap>pageheap

mytest.exe: page heap enabled with flags (full traces )

当激活pageheap后,重新运行一次上面的代码,程序就崩溃了。

(直接双击运行程序和在Windbg中用调试模式运行程序,观察到的崩溃有差别。在Windbg中运行,pageheap会首先触发break point异常,同时pageheap还会在调试器中输出额外的调试信息方便调试。)

上面的例子说明了pageheap能够让错误尽快暴露出来。接下来我们稍微修改一下代码:

   char *p=(char*)malloc(1023);

   p[1023]=1;

试试看,修改后的代码还会导致程序崩溃吗?

根据我的测试,分配1023字节的情况下,哪怕激活pageheap,也不会崩溃。你能说明原因吗?如果看不出来,可以检查一下每次malloc返回的地址的数值,注意对这个数值在二进制上敏感一点,然后结合Heap Manager和pageheap的原理思考一下,看看有没有发现。

对于上面两种代码,如果用debug模式编译,激活pageheap,程序会崩溃吗?根据我的测试,无论是否激活pageheap,debug模式都不会崩溃的。你能想到原因吗?

再来看下面一段代码:

   char *p=(char*)malloc(1023);

   free(p);

   free(p);

这里显然有double free的问题。

如果没有激活pageheap,分别在debug和release模式下运行,根据我的测试,debug模式下会崩溃,release模式下运行正常。

如果激活pageheap,同样在debug/release模式下运行。根据我的测试,在两种模式下都会崩溃。如果细心观察,会发现两种模式下,崩溃后弹出的提示各自不同。你能想到原因吗?

如果有兴趣,你还可以测试一下heap误用的其他几种情况,看看pageheap是不是都有帮助。

Heap上的内存泄漏和内存碎片

从上面的例子,可以很清楚地看到pageheap对于检查这类问题的帮助。同时也可以看到,pageheap无法保证检查出所有潜在问题,比如分配1023个字节,但是写1024个字节这种情况。只有理解pageheap的工作原理,同时对问题作认真的思考和测试后,才会理解其中的差别。

除了Heap使用不当导致崩溃外,还有一类问题是内存泄漏。内存泄漏是指随着程序的运行,内存消耗越来越多,最后发生内存不足,或者整体性能下降。从代码上看,这类问题是由于内存使用后没有及时释放导致的。这里的内存,可以是VirtualAlloc分配的,也有可能是HeapAllocate分配的。

这里只讨论Heap相关的内存泄漏。检查内存泄漏是一个比较大的题目,第4章会作详细讨论。

举个例子,客户开发一个cd刻录程序。每次把盘片中所有内容写入内存,然后开始刻录。如果每次刻录完成后都忘记去释放分配的空间,那么最多能够刻3张CD。因为3张CD,每一张600MB,加在一起就是1.8GB,濒临2GB的上限。

另外还有一种跟内存泄漏相关的问题,是内存碎片(Fragmentation)。内存碎片是指内存被分割成很多的小块,以至于很难找到连续的内存来满足比较大的内存申请。导致内存碎片常见原因有两种,一种是加载了过多DLL,还有一种是小块Heap的频繁使用。

DLL分割内存空间最常见的情况是ASP.NET中的batch compilation没有打开,导致每一个ASP.NET页面都会被编译成一个单独的DLL文件。运行一段时间后,就可以看到几千个DLL文件加载到进程中。一个极端的例子是5000个DLL把2GB内存平均分成5000份,导致每一份的大小在400KB左右(假设DLL本身只占用1个字节),于是无法申请大于400KB的内存,哪怕总的内存还是接近2GB。对于这种情况的检查很简单,列一下当前进程中所有加载起来的DLL就可以看出问题来。

对于小块Heap的频繁使用导致的内存分片,可以参考下面的解释:

Heap fragmentation is often caused by one of the following two reasons

1. Small heap memory blocks that are leaked (allocated but never freed) over time

2. Mixing long lived small allocations with short lived long allocations

Both of these reasons can prevent the NT heap manager from using free memory efficiently since they are spread as small fragments that cannot be used as a single large allocation

为了更好地理解上面的解释,考虑这样的情况。假设开发人员设计了一个数据结构来描述一首歌曲,数据结构分成两部分,第一部分是歌曲的名字、作者和其他相关的描述性信息,第二部分是歌曲的二进制内容。显然第一部分比第二部分小得多。假设第一部分长度1KB,第二部分399KB。每处理一首歌需要调用两次内存分配函数,分别分配数据结构第一部分和第二部分需要的空间。

假设每次处理完成后,只释放了数据结构的第二部分,忘记释放第一部分,这样每处理一次,就会留下1个1KB的数据块没有释放。程序长时间运行后,留下的1KB数据块就会很多,虽然HeapManager的薄计信息中可能记录了有很多399KB的数据块可以分配,但是如果要申请500KB的内存,就会因为找不到连续的内存块而失败。对于内存碎片的调试,可以参考最后的案例讨论。在Windows 2000上,可以用下面的方法来缓解问题:

The Windows XP Low Fragmentation Heap Algorithm Feature Is Available for Windows 2000

http://support.microsoft.com/?id=816542

关于 CLR上内存碎片的讨论和图文详解,请参考:

.NET Memory usage - A restaurant analogy

2.4.3  Stack overrun/corruption

另外一种内存问题是Stack overrun和Stack corruption。Stack overrun很简单,一般是递归函数缺少结束条件导致,函数调用过深从而把stack地址用光,比如下面的代码:

Void foo()

{

foo();

}

只要在调试器里重现问题,调试器立刻就会收到Stack overflow Exception。检查callstack就可以立刻看出问题所在:

0:001> g

(cd0.4b0): Stack overflow - code c00000fd (first chance)

First chance exceptions are reported before any exception handling.

This exception may be expected and handled.

eax=cccccccc ebx=7ffdd000 ecx=00000000 edx=10312d18 esi=0012fe9c edi=00033130

eip=004116f9 esp=00032f9c ebp=0003305c iopl=0    nv up ei pl nz na po nc

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

*** WARNING: Unable to verify checksum for c:\Documents and Settings\Li Xiong\My Documents\My code\MyTest\debug\MyTest.exe

MyTest!foo+0x9:

004116f9 53               push    ebx

0:000> k

ChildEBP RetAddr 

0003305c 00411713 MyTest!foo+0x9

00033130 00411713 MyTest!foo+0x23

00033204 00411713 MyTest!foo+0x23

000332d8 00411713 MyTest!foo+0x23

000333ac 00411713 MyTest!foo+0x23

00033480 00411713 MyTest!foo+0x23

00033554 00411713 MyTest!foo+0x23

00033628 00411713 MyTest!foo+0x23

000336fc 00411713 MyTest!foo+0x23

000337d0 00411713 MyTest!foo+0x23

000338a4 00411713 MyTest!foo+0x23

00033978 00411713 MyTest!foo+0x23

00033a4c 00411713 MyTest!foo+0x23

第二种情况,Stack corruption往往是Stack buffer overflow导致的。这样的bug不单单会造成程序崩溃,还会严重威胁到系统安全性。在网上搜索Stack buffer overflow,可以看到无数用Stack buffer进行攻击的例子。

在当前的计算机架构上,Stack是保存运行信息的地方。当Stack损坏后,当前执行情况的所有信息都丢失了,所以调试器在这种情况下没有用武之地。比如下面的代码:

void killstack()

{

  char c; 

  char *p=&c;

  for(int i=10;i<=100;i++)

    *(p+i)=0; 

}

int main(int, char*)

{

  killstack();

  return 0;

}

在VS2005中用下面的参数,在debug模式下编译:

/Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /Gm /EHsc /RTC1 /MDd /Gy /Fo"Debug\\" /Fd"Debug\vc80.pdb" /W3 /nologo /c /Wp64 /Zi /TP /errorReport:prompt

在调试器中运行,看到的结果是:

0:000> g

ModLoad: 76290000 762ad000   C:\WINDOWS\system32\IMM32.DLL

ModLoad: 62d80000 62d89000   C:\WINDOWS\system32\LPK.DLL

ModLoad: 75490000 754f1000   C:\WINDOWS\system32\USP10.dll

(1d0.1504): Access violation - code c0000005 (first chance)

First chance exceptions are reported before any exception handling.

This exception may be expected and handled.

eax=0012ff5b ebx=7ffda000 ecx=fffffffb edx=0012ffbf esi=00000000 edi=00000000

eip=000000f8 esp=0012ff68 ebp=0012ff68 iopl=0         nv up ei pl zr na po nc

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

000000f8 ??               ???

0:000> kb

ChildEBP RetAddr  Args to Child             

WARNING: Frame IP not in any known module. Following frames may be wrong.

0012ff64 00000000 00000000 00000000 00000000 0xf8

0:000> dds esp

0012ff68  00000000

0012ff6c  00000000

0012ff70  00000000

0012ff74  00000000

0012ff78  00000000

0012ff7c  00000000

0012ff80  00000000

0012ff84  00000000

0012ff88  00000000

0012ff8c  00000000

0012ff90  00000000

0012ff94  00000000

0012ff98  00000000

0012ff9c  00000000

0012ffa0  00000000

0012ffa4  00000000

0012ffa8  00000000

0012ffac  00000000

0012ffb0  00000000

0012ffb4  00000000

0012ffb8  00000000

0012ffbc  00000000

0012ffc0  0012fff0

0012ffc4  77e523cd kernel32!BaseProcessStart+0x23

在Windbg里面看到EIP,EBP都指向非法地址,callstack的信息已经被冲毁,根本找不到任何线索进行调试。对于Stack corruption,行之有效的方法是首先对问题作大致定位,然后检查相关函数,在可疑函数中添加代码写log文件。当问题发生后从log文件中找到线索。

2.4.4  题外话和相关讨论

PageHeap的/unaligned参数

 

char *p=(char*)malloc(1023);

   p[1023]=1;

前面提到了分配1023个字节的问题。在激活pageheap后,同时使用/unaligned参数,才可以检测到这类问题。详细情况请参考KB816542中关于/unaligned的介绍。

同理,下面这段代码默认情况下使用pageheap也不会崩溃:

   char *p=new char[1023];

   p[-1]='c';

解决方法是使用pageheap的/backwards参数。

上面两个例子说明由于4KB的粒度限制,哪怕使用pageheap,也需要根据pageheap的原理来调整参数,以便覆盖多种情况。

Heap trace,系统帮你记录下每次Heap的操作

Pageheap的另外一个功能是trace,作用是记录Heap的历史操作。激活pageheap的trace功能后,Heap Manager会在内存中开辟一块专门的空间来记录每次Heap的操作,比如Heap的分配和释放,把操作Heap的callstack记录下来。当问题发生后,在Windbg中可以检查Heap操作的历史记录,方便调试。参考下面一个例子:

char * getmem()

{

   return new char[100];

}

void free1(char *p)

{

   delete p;

}

void free2(char *p)

{

   delete [] p;

}

int main(int, char*)

{

   char *c=getmem();

   free1(c);

   free2(c);

   return 0;

}

该程序在release模式下,不激活pageheap是不会崩溃的。激活pageheap后,在Windbg中运行会看到:

0:000> g

===========================================================

VERIFIER STOP 00000007: pid 0x1324: block already freed

  015B1000 : Heap handle

  003F5858 : Heap block

  00000064 : Block size

  00000000 :

===========================================================

(1324.538): Break instruction exception - code 80000003 (first chance)

eax=00000000 ebx=015b1001 ecx=7c81b863 edx=0012fa7f esi=00000064 edi=00000000

eip=7c822583 esp=0012fbe8 ebp=0012fbf4 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:

7c822583 cc               int     3

激活pageheap后,当Heap Manager检测到错误,就会激发一个break point exception,使debugger停下来,同时pageheap会在debugger中打印出block already freed信息,表示这是一个double free问题。

通过kb命令可以打印出当前的callstack。注意,由于崩溃发生在第二次调用Free函数的时候,所以这里看到的是第二次调用Free函数时的callstack:

0:000> kb

ChildEBP RetAddr  Args to Child             

0012fbe4 7c85079b 015b1000 0012fc94 0012fc70 ntdll!DbgBreakPoint

0012fbf4 7c87204b 00000007 7c8722f8 015b1000 ntdll!RtlpPageHeapStop+0x72

0012fc70 7c873305 015b1000 00000004 003f5858 ntdll!RtlpDphReportCorruptedBlock+0x11e

0012fca0 7c8734c3 015b1000 003f0000 01001002 ntdll!RtlpDphNormalHeapFree+0x32

0012fcf8 7c8766b9 015b0000 01001002 003f5858 ntdll!RtlpDebugPageHeapFree+0x146

0012fd60 7c860386 015b0000 01001002 003f5858 ntdll!RtlDebugFreeHeap+0x1ed

0012fe38 7c81d77d 015b0000 01001002 003f5858 ntdll!RtlFreeHeapSlowly+0x37

0012ff1c 78134c3b 015b0000 01001002 003f5858 ntdll!RtlFreeHeap+0x11a

0012ff68 00401016 003f5858 003f5858 00000064 MSVCR80!free+0xcd

0012ff7c 00401198 00000001 003f57e8 003f3628 win32!main+0x16 [d:\xiongli\today\win32\win32\win32.cpp @ 77]

0012ffc0 77e523cd 00000000 00000000 7ffde000 win32!__tmainCRTStartup+0x10f

0012fff0 00000000 004012e1 00000000 78746341 kernel32!BaseProcessStart+0x23

发生崩溃的Free函数调用(后面一次Free调用)的返回地址是00401016,所以后面一次Free是在00401016的前一行被调用的。接下来分析第一次Free调用发生的地方。发生问题的Heap地址是Free函数的参数0x3f5858,在Windbg中使用!heap命令加上–p –a参数打印出保存下来的callstack:

0:000> !heap -p -a 0x3f5858

    address 003f5858 found in

    _HEAP @ 3f0000

   in HEAP_ENTRY: Size : Prev Flags - UserPtr UserSize - state

        3f5830: 0014 : N/A  [N/A] - 3f5858 (70) - (free DelayedFree)

        Trace: 004f

        7c860386 ntdll!RtlFreeHeapSlowly+0x00000037

        7c81d77d ntdll!RtlFreeHeap+0x0000011a

        78134c3b MSVCR80!free+0x000000cd

        401010 win32!main+0x00000010

        77e523cd kernel32!BaseProcessStart+0x00000023

上面的callstack就是Heap Manager保存的,这是Heap地址的历史操作。从保存的callstack看到,在0x401010地址是MSVCR80!free调用的返回地址,所以00401016和00401010两个地址,就是对同一个Heap地址两次调用Free后的两个返回地址。检查这两个地址的前一条汇编语句,就能找到对应的Free调用:

0:000> uf 00401010

win32!main [d:\xiongli\today\win32\win32\win32.cpp @ 74]:

   74 00401000 56               push    esi

   75 00401001 6a64             push    0x64

   75 00401003 e824000000       call    win32!operator new[] (0040102c)

   75 00401008 8bf0             mov     esi,eax

   76 0040100a 56               push    esi

   76 0040100b e828000000       call    win32!operator delete (00401038)

   77 00401010 56               push    esi

   77 00401011 e81c000000       call    win32!operator delete[] (00401032)

   77 00401016 83c40c           add     esp,0xc

   78 00401019 33c0             xor     eax,eax

   78 0040101b 5e               pop     esi

   79 0040101c c3               ret

这里可以看到,对应的问题的确是前后调用delete和delete []导致的。对应的源代码地址大约在win32.cpp的74行。(源代码中,delete和delete[]是在两个自定义函数中被调用的。这里看不到free1和free2两个函数的原因在于release模式下编译器做了inline优化。)

同时可以检查一下Heap 指针0x3f5858前后的内容:

0:000> dd 0x3f5848

003f5848  7c88c580 0025a5f0 00412920 dcbaaaa9

003f5858  f0f0f0f0 f0f0f0f0 f0f0f0f0 f0f0f0f0

003f5868  f0f0f0f0 f0f0f0f0 f0f0f0f0 f0f0f0f0

003f5878  f0f0f0f0 f0f0f0f0 f0f0f0f0 f0f0f0f0

003f5888  f0f0f0f0 f0f0f0f0 f0f0f0f0 f0f0f0f0

003f5898  f0f0f0f0 f0f0f0f0 f0f0f0f0 f0f0f0f0

003f58a8  f0f0f0f0 f0f0f0f0 f0f0f0f0 f0f0f0f0

003f58b8  f0f0f0f0 a0a0a0a0 a0a0a0a0 00000000

这里的红色dcba其实是一个标志位,标志位前面的地址保存的其实就是这个Heap地址的历史操作记录。通过Windbg的dds命令,可以直接检查保存下来的callstack:

0:000> dds 00412920

00412920  00000000

00412924  00000001

00412928  0005004f

0041292c  7c860386 ntdll!RtlFreeHeapSlowly+0x37

00412930  7c81d77d ntdll!RtlFreeHeap+0x11a

00412934  78134c3b MSVCR80!free+0xcd

00412938  00401010 win32!main+0x10

0041293c  77e523cd kernel32!BaseProcessStart+0x23

了解这个标志位的好处是,可以利用这个特点来解决memory leak和fragmentation。由于发生泄漏的内存往往是相同callstack分配的,所以泄漏比较严重的程度时,程序中残留的大多数的Heap指针都是泄漏掉的内存地址。通过在程序中搜索每一个Heap 指针的标志位,就可以找到这些指针分别对应的callstack。如果某些callstack出现得非常频繁,这些callstack往往就跟memory leak相关。下面就是一个使用这个方法解决memory leak的案例。

为何才分配了300MB内存,就报告Out of memory

客户程序在内存占用只有300MB左右的时候,对malloc的调用就会失败。通过检查问题发生时候的dump文件,发现问题是由heap fragmentation导致的。客户的程序有大量的小块内存没有及时释放,导致分片严重。

激活pageheap后,再次抓取问题发生时的dump,然后使用下面命令在内存空间搜索dcba标志位:

0:044> s -w 0 L?60030000      0xdcba

00115e9e  dcba 0000 0000 ef98 0012 893d 0047 efc8  ..........=.G...

19b90fe6  dcba cfe8 02d8 afe8 2ca3 cfe8 02d8 b22a  .........,....*.

19b92fe6  dcba cfe8 1a52 8fe8 1dff cfe8 1af6 f44f  ....R.........O.

19b9cfce  dcba efd0 23d8 cfd0 1c58 8fd0 15ac c0c0  .....#..X.......

2b06efe6  dcba cfe8 02d8 8fe8 258b cfe8 02d8 a6d2  .........%......

2b074fce  dcba 2fd0 1c0f afd0 1c4d dfd0 0e69 c0c0  .../....M...i...

2e860fe6  dcba afe8 02d8 2fe8 2ef3 afe8 02d8 0a0b  ......./........

2e868fce  dcba afd0 0881 2fd0 2e92 afd0 0881 c0c0  ......./........

根据搜索结果,使用下面的命令来随机打印callstack,看到:

0:044> dds poi(19b92fe6  -6)

005bba0c  005cbe90

005bba10  00031c49

005bba14  00122ddb

005bba18  77fa8468 ntdll!RtlpDebugPageHeapAllocate+0x2f7

005bba1c  77faa27a ntdll!RtlDebugAllocateHeap+0x2d

005bba20  77f60e22 ntdll!RtlAllocateHeapSlowly+0x41

005bba24  77f46f5c ntdll!RtlAllocateHeap+0xe3a

005bba28  0046b404 Customer_App+0x6b404

005bba2c  0046b426 Customer_App+0x6b426

005bba30  00427612 Customer_App+0x27612

0:044> dds poi(19b9cfce  -6)

005bba0c  005cbe90

005bba10  00031c49

005bba14  00122ddb

005bba18  77fa8468 ntdll!RtlpDebugPageHeapAllocate+0x2f7

005bba1c  77faa27a ntdll!RtlDebugAllocateHeap+0x2d

005bba20  77f60e22 ntdll!RtlAllocateHeapSlowly+0x41

005bba24  77f46f5c ntdll!RtlAllocateHeap+0xe3a

005b8024  0046b404 Customer_App+0x6b404

005b8028  0046b426 Customer_App+0x6b426

005b802c  00427a82 Customer_App+0x27a82

0:044> dds poi(2b06efe6  -6)

005bba0c  005cbe90

005bba10  00031c49

005bba14  00122ddb

005bba18  77fa8468 ntdll!RtlpDebugPageHeapAllocate+0x2f7

005bba1c  77faa27a ntdll!RtlDebugAllocateHeap+0x2d

005bba20  77f60e22 ntdll!RtlAllocateHeapSlowly+0x41

005bba24  77f46f5c ntdll!RtlAllocateHeap+0xe3a

005bd5d4  0046b404 Customer_App+0x6b404

005bd5d8  0046b426 Customer_App+0x6b426

005bd5dc  00427612 Customer_App+0x27612

正常情况下,内存指针分配的callstack是随机的。但是上面的却看到大多数内存指针都由固定的callstack分配,该callstack很有可能就是泄漏的根源。拿到客户的PDB文件后,把偏移跟源代码对应起来,很快就找到了申请这些内存的源代码。客户检查源代码后发现这就是问题根源。添加对应的内存释放代码后,问题解决。

原文地址:https://www.cnblogs.com/chengxin1985/p/1546196.html