示例 12:使用页堆验证查找 Bug

以下一系列命令演示如何使用 GFlags 和 NTSD 调试器的页面堆验证功能来检测堆内存使用中的错误。 在此示例中,程序员怀疑虚构应用程序(pheap-buggy.exe)存在堆错误,并继续进行一系列测试以识别错误。

有关 NTSD 的详细信息,请参阅 使用 CDB 和 NTSD 进行调试

步骤 1:启用标准页堆验证

以下命令为pheap-buggy.exe启用标准页堆验证:

gflags /p /enable pheap-buggy.exe

步骤 2:验证页面堆是否已启用

以下命令列出为其启用了页堆验证的图像文件:

gflags /p

作为响应,GFlags 显示以下程序列表。 在此显示中, 跟踪 指示标准页堆验证, 完整跟踪 表示整页堆验证。 在这种情况下,pheap-buggy.exe会列出 跟踪,指示已按预期启用标准页面堆验证。

pheap-buggy.exe: page heap enabled with flags (traces )

步骤 3:运行调试器

以下命令在 NTSD 中运行 pheap-buggy.exe 的 CorruptAfterEnd 函数,其中 -g (忽略初始断点) , () 参数对访问冲突异常设置第二次机会中断:

ntsd -g -x pheap-buggy CorruptAfterEnd

应用程序发生故障时,NTSD 将生成以下显示,指示它检测到pheap-buggy.exe错误:

===========================================================
VERIFIER STOP 00000008: pid 0xAA0: corrupted suffix pattern

        00C81000 : Heap handle 
        00D81EB0 : Heap block 
        00000100 : Block size 
#         00000000 :
===========================================================

Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00d81eb0 ecx=77f7e257 edx=0006fa18 esi=00000008 edi=00c81000
eip=77f7e098 esp=0006fc48 ebp=0006fc5c iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
ntdll!DbgBreakPoint:
77f7e098 cc               int     3

标头信息包括包含损坏块的堆的地址 (00C81000 :堆句柄) 、损坏块的地址 (00D81EB0:堆块) ,以及分配大小 (00000100:块大小) 。

“已损坏后缀模式”消息指示应用程序违反了 GFlags 在pheap-buggy.exe堆分配结束后插入的数据完整性模式。

步骤 4:显示调用堆栈

在下一步中,使用 NTSD 报告的地址查找导致错误的函数。 接下来的两个命令在调试器中打开行号转储,并使用行号显示调用堆栈。

C:\>.lines

Line number information will be loaded 

C:\>kb

ChildEBP RetAddr  Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
0006fc5c 77f9e6dd 00000008 77f9e3e8 00c81000 ntdll!DbgBreakPoint
0006fcd8 77f9f3c8 00c81000 00000004 00d81eb0 ntdll!RtlpNtEnumerateSubKey+0x2879
0006fcfc 77f9f5bb 00c81000 01001002 00000010 ntdll!RtlpNtEnumerateSubKey+0x3564
0006fd4c 77fa261e 00c80000 01001002 00d81eb0 ntdll!RtlpNtEnumerateSubKey+0x3757
0006fdc0 77fc0dc2 00c80000 01001002 00d81eb0 ntdll!RtlpNtEnumerateSubKey+0x67ba
0006fe78 77fbd87b 00c80000 01001002 00d81eb0 ntdll!RtlSizeHeap+0x16a8
0006ff24 010013a4 00c80000 01001002 00d81eb0 ntdll!RtlFreeHeap+0x69
0006ff3c 01001450 00000000 00000001 0006ffc0 pheap-buggy!TestCorruptAfterEnd+0x2b [d:\nttest\base\testsrc\kernel\rtl\pageheap\pheap-buggy.cxx @ 185]
0006ff4c 0100157f 00000002 00c65a68 00c631d8 pheap-buggy!main+0xa9 [d:\nttest\base\testsrc\kernel\rtl\pageheap\pheap-buggy.cxx @ 69]
0006ffc0 77de43fe 00000000 00000001 7ffdf000 pheap-buggy!mainCRTStartup+0xe3 [crtexe.c @ 349]
0006fff0 00000000 0100149c 00000000 78746341 kernel32!DosPathToSessionPathA+0x204

因此,调试器会显示具有行号的pheap-buggy.exe的调用堆栈。 调用堆栈显示显示,当 pheap-buggy.exe 中的 TestCorruptAfterEnd 函数尝试通过调用 HeapFree(一种重定向到 RtlFreeHeap)在0x00c80000释放分配时发生错误。

此错误的最可能原因是程序写入了它在此函数中分配的缓冲区末尾。

步骤 5:启用整页堆验证

与标准页堆验证不同,整页堆验证可以在发生此堆缓冲区后立即捕获误用。 以下命令为pheap-buggy.exe启用整页堆验证:

gflags /p /enable pheap-buggy.exe /full

步骤 6:验证是否启用了整页堆

以下命令列出为其启用了页堆验证的程序:

gflags /p

作为响应,GFlags 显示以下程序列表。 在此显示中, 跟踪 指示标准页堆验证, 完整跟踪 表示整页堆验证。 在这种情况下,pheap-buggy.exe列出 包含完整跟踪,指示已按预期启用整页堆验证。

pheap-buggy.exe: page heap enabled with flags (full traces )

步骤 7:再次运行调试器

以下命令在 NTSD 调试器中运行 pheap-buggy.exe 的 CorruptAfterEnd 函数,其中 -g (忽略初始断点) 和 -x (设置第二次机会中断的访问冲突异常) 参数:

ntsd -g -x pheap-buggy CorruptAfterEnd

应用程序发生故障时,NTSD 将生成以下显示,指示它检测到pheap-buggy.exe错误:

Page heap: process 0x5BC created heap @ 00880000 (00980000, flags 0x3)
ModLoad: 77db0000 77e8c000   kernel32.dll
ModLoad: 78000000 78046000   MSVCRT.dll
Page heap: process 0x5BC created heap @ 00B60000 (00C60000, flags 0x3)
Page heap: process 0x5BC created heap @ 00C80000 (00D80000, flags 0x3)
Access violation - code c0000005 (first chance)
Access violation - code c0000005 (!!! second chance !!!)
eax=00c86f00 ebx=00000000 ecx=77fbd80f edx=00c85000 esi=00c80000 edi=00c16fd0
eip=01001398 esp=0006ff2c ebp=0006ff4c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000206
pheap-buggy!TestCorruptAfterEnd+1f:
01001398 889801010000     mov     [eax+0x101],bl          ds:0023:00c87001=??

启用整页堆验证后,调试器会在访问冲突时中断。 若要查找访问冲突的确切位置,请打开行号转储并显示调用堆栈跟踪。

编号的调用堆栈跟踪如下所示:

ChildEBP RetAddr  Args to Child
0006ff3c 01001450 00000000 00000001 0006ffc0 pheap-buggy!TestCorruptAfterEnd+0x1f [d:\nttest\base\testsrc\kernel\rtl\pageheap\pheap-buggy.cxx @ 184]
0006ff4c 0100157f 00000002 00c16fd0 00b70eb0 pheap-buggy!main+0xa9 [d:\nttest\base\testsrc\kernel\rtl\pageheap\pheap-buggy.cxx @ 69]
0006ffc0 77de43fe 00000000 00000001 7ffdf000 pheap-buggy!mainCRTStartup+0xe3 [crtexe.c @ 349]
WARNING: Stack unwind information not available. Following frames may be wrong.
0006fff0 00000000 0100149c 00000000 78746341 kernel32!DosPathToSessionPathA+0x204

堆栈跟踪显示问题出现在pheap-buggy.exe的第 184 行中。 由于启用了整页堆验证,因此调用堆栈在程序代码中启动,而不是在系统 DLL 中启动。 因此,在发生冲突的位置捕获了违规行为,而不是在释放堆块时捕获。

步骤 8:在代码中找到错误

快速检查可揭示问题原因:程序尝试写入 256 字节 (0x100) 缓冲区的第 257 个字节 (0x101) ,这是一个常见的逐一错误。

*((PCHAR)Block + 0x100) = 0;