调试器安装和设置
某些应用程序验证器操作可能会导致引发异常。 必须将调试器设置为在第二次机会上捕获这些异常,因为应用程序验证程序本身将处理第一个机会异常。
引发的异常有三种类型:
如果堆选项检测到堆缓冲区溢出,则会生成访问冲突异常(0xC0000005)。 在某些情况下,“检查系统路径使用”选项也可能导致访问冲突。
当“检测无效句柄使用”选项检测到无效句柄操作时,将生成无效的句柄异常(0xC0000008)。
当检查足够的堆栈选项检测到初始堆栈太短时,将生成堆栈溢出异常(0xC00000FD)。
准备这些事件的一种方法是在命令行上启动调试器,如下所示:
windbg -xd av -xd ch -xd sov ApplicationCommandLine
或
cdb -xd av -xd ch -xd sov ApplicationCommandLine
如果已启动调试器,可以使用 sxd(设置异常)命令捕获所有访问冲突、无效句柄和堆栈溢出作为第二次异常:
0:000> sxd av
0:000> sxd ch
0:000> sxd sov 1
从理论上讲,可以通过内核调试器控制应用程序验证程序。 但是,不建议这样做 — 它需要频繁使用 .process 和 .pagein 命令,但它提供的功能不如使用用户模式调试器。
安装调试工具
若要下载最新版本的工具,请参阅 下载适用于 Windows 的调试工具。
为 User-Mode 调试配置硬件
用户模式调试通常在单个计算机上完成:调试器在失败的应用程序所在的同一台计算机上运行。
在这种情况下,不需要特定的硬件设置。 在整个本主题中,术语主机和目标计算机在此示例中是可互换的。
为 User-Mode 调试配置软件
基本 User-Mode 配置 - 在开始用户模式调试之前,必须下载必要的符号文件并设置某些环境变量。
符号文件
必须下载正在调试的用户模式进程的符号文件。 如果这是你编写的应用程序,则应使用完整的符号文件生成它。 如果它是商业应用程序,则符号文件可能在 Web 服务器上可用或下载,请联系制造商。
如果要执行远程调试,符号文件位置取决于所使用的方法:
如果要通过调试器执行远程调试,符号文件应位于具有调试服务器的计算机上。
如果要通过 remote.exe执行远程调试,符号文件应位于具有调试器的计算机上。
如果要通过进程服务器或 KD 连接服务器执行远程调试,符号文件应位于具有智能客户端的计算机上。
如果要从内核调试器控制用户模式调试器,则符号文件需要位于这两台计算机上。
配置环境变量
调试器使用各种环境变量来指示许多重要设置。
有关调试器的详细信息,请参阅 Windows 调试入门
使用命令行通过调试器配置应用程序验证程序
若要配置应用程序验证程序,可以使用 CDB 或 NTSD 命令行。
使用以下命令行:
cdb OtherOptions -vf:Flags Target
其中,Target 是目标应用程序的名称,Flags 指定要应用于此目标的所需应用程序验证程序选项。
标志应是表示所需选项的位之和。 各个位值如下所示:
| 标志值 | 含义 |
|---|---|
| 00000001 | 堆检查 |
| 00000004 | 句柄检查 |
| 00000008 | 低资源消耗 SIM 卡检查 |
| 00000020 | TLS 检查 |
| 00000040 | 脏堆栈 |
| 00000200 | 危险 API |
| 00001000 | 异常检查 |
| 00002000 | 内存检查 |
| 00020000 | 杂项检查 |
| 00040000 | 锁机制检查 |
使用 !avrf 进行调试
!avrf 扩展控制应用程序验证程序的设置,并显示应用程序验证程序生成的各种输出。 有关 !arvrf 扩展的其他信息,请参阅调试器文档中的 !avrf。
语法
!avrf
不带任何参数的 !avrf 命令显示应用程序验证程序设置以及有关当前和以前的应用程序验证程序的信息(如果有)。
!avrf –vs { Length | -aAddress }
显示虚拟空间操作日志。 长度指定要从最新开始显示的记录数。 地址指定虚拟地址。 将显示包含此虚拟地址的虚拟操作记录。
!avrf -hp { Length | -a Address }
显示堆操作日志。 地址用于指定堆地址。 将显示包含此堆地址的堆操作记录。
!avrf -cs { Length | -a Address }
显示临界区删除日志。 长度指定要从最新开始显示的记录数。 地址指定临界区地址。 指定地址时,将显示特定关键节的记录。
!avrf -dlls [ Length ]
显示 DLL 加载/卸载日志。 长度指定要从最新开始显示的记录数。
!avrf -trm
显示所有已终止和挂起的线程的日志。
!avrf -ex [ Length ]
显示异常日志。 应用程序验证程序跟踪应用程序中发生的所有异常。
!avrf -threads [ ThreadID ]
显示目标进程中有关线程的信息。 对于子线程,也会显示父级指定的堆栈大小和 CreateThread 标志。 提供线程 ID 将仅显示该特定线程的信息。
!avrf -tp [ ThreadID ]
显示线程池日志。 此日志可能包含各种操作的堆栈跟踪,例如更改线程关联掩码、更改线程优先级、发布线程消息、初始化 COM 以及在线程池回调中取消初始化 COM。 提供线程 ID 将仅显示该特定线程的信息。
!avrf -srw [ Address | Address Length ] [ -stats ]
显示精简读取器/编写器(SRW)日志。 指定地址将显示与该 SRW 锁定地址相关的记录。 与地址一起指定 Length 时,将显示该地址范围内的所有 SRW 锁。 -stats 选项导出 SRW 锁统计信息。
!avrf -leak [ -m ModuleName ] [ -r ResourceType ] [ -a Address ] [ -t ]
显示未完成的资源日志。 这些资源在任何时刻都可能或者可能不发生泄漏。 指定模块名称(包括扩展名)显示指定模块中的所有未处理的资源。 指定 ResourceType 将显示该特定资源类型的未完成资源。 指定地址转储与该地址相关的未完成任务的资源记录。 ResourceType 可以是下列项之一:
- 堆:使用 Win32 堆 API 显示堆分配
- 本地:显示本地/全局分配
- CRT:使用 CRT API 显示分配
- 虚拟:显示虚拟预定
- BSTR:显示 BSTR 分配
- 注册表:显示注册表项打开
- 电源:显示电源通知对象
- 句柄:显示线程、文件和事件句柄分配
!avrf –trace TraceIndex
显示指定追踪索引的堆栈跟踪。 某些结构使用此 16 位索引号来标识堆栈跟踪。 此索引指向堆栈跟踪数据库中的位置。 如果要分析此类结构,你会发现此语法很有用。
!avrf -cnt
显示全局计数器的列表。
!avrf -brk [ BreakEventType ]
指定这是中断事件命令。 在未使用其他参数的情况下,使用!avrf -brk将显示中断事件设置。 BreakEventType 指定中断事件的类型编号。 有关可能类型的列表,请使用 !avrf -brk。
!avrf -flt [ EventTypeProbability ]
指定这是错误注入命令。 在不使用其他参数时使用!avrf -flt,将显示当前的故障注入设置。 EventType 指定事件的类型号。 概率指定事件将失败的频率。 这可以是介于 0 和 1,000,000 之间的任意整数(0xF4240)。
!avrf -flt break EventType
每次注入此错误时,应用程序验证程序都会中断调试器。
!avrf -flt stacks Length
显示最近错误注入操作的堆栈跟踪数量。
!avrf -trg [ StartEnd | dll Module | all ]
指定这是一个目标范围命令。 当 -trg 与没有其他参数一起使用时,将显示当前目标范围。 Start 指定目标范围或排除范围的起始地址。 End 指定目标范围或排除范围的结束地址。 模块指定要作为目标或排除的模块的名称。 模块应包含完整的模块名称,包括 .exe 或 .dll 扩展。 不应包含路径信息。 指定所有会导致重置所有目标范围或排除范围。
!avrf -skp [ StartEnd | dll Module | all | Time ]
指定这是排除范围命令。 Start 指定目标范围或排除范围的起始地址。 End 指定目标范围或排除范围的结束地址。 模块指定要作为目标或排除的模块的名称。 模块应包含完整的模块名称,包括 .exe 或 .dll 扩展。 不应包含路径信息。 指定所有会导致重置所有目标范围或排除范围。 指定时间会在执行恢复后的时间毫秒内抑制所有故障。
以下是调试器中 !avrf 命令提供的输出。
0:000> !avrf
Application verifier settings (816431A7):
- full page heap
- COM
- RPC
- Handles
- Locks
- Memory
- TLS
- Exceptions
- Threadpool
- Leak
- SRWLock
No verifier stop active.
Note: Sometimes bugs found by verifier manifest themselves as raised
exceptions (access violations, stack overflows, invalid handles),
and it is not always necessary to have a verifier stop.
!avrf 扩展注释
当 !avrf 扩展与无参数一起使用时,它将显示当前的应用程序验证程序选项。
!avrf 扩展使用调试器中的 Exts.dll。
如果发生了应用程序验证程序停止,则不带参数的 !avrf 扩展将显示停止的性质及其原因。
如果缺少 ntdll.dll 和 verifier.dll 的符号,!avrf 扩展将生成错误消息。
可连续和不可连续中断
调试可连续停止
下面是由于检测无效句柄使用的选项而引发的无效句柄异常的一个示例。
首先,将显示以下消息:
Invalid handle - code c0000008 (first chance)
===================================================
VERIFIER STOP 00000300: pid 0x558: invalid handle exception for current stack trace
C0000008 : Exception code.
0012FBF8 : Exception record. Use .exr to display it.
0012FC0C : Context record. Use .cxr to display it.
00000000 :
===================================================
This verifier stop is continuable.
After debugging it use 'go' to continue.
===================================================
Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=6a27c280 ecx=6a226447 edx=0012fa4c esi=00942528 edi=6a27c260
eip=6a22629c esp=0012facc ebp=0012faf0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
6a22629c cc int 3
请注意,消息告诉您可以继续此应用程序验证程序的停止过程。 了解已发生的内容后,可以继续运行目标应用程序。
首先,应使用 !avrf 扩展。 这会提供有关当前失败的信息:
0:000> !avrf
Global flags: 00000100
Application verifier global flag is set.
Application verifier settings (00000004):
- no heap checking enabled!
- handle checks
Page heap is not active for this process.
Current stop 00000300 : c0000008 0012fbf8 0012fc0c 00000000 .
Using an invalid handle (either closed or simply bad).
此显示的最后一行总结了问题。
此时可能需要查看一些日志。 完成后,使用 g (Go) 命令再次启动应用程序:
0:000> g
## Debugging a Non-Continuable Stop
Here is an example of an access violation that has been raised by the page heap option.
First, the following message appears:
Access violation - code c0000005 (first chance)
===================================================
VERIFIER STOP 00000008: pid 0x504: exception raised while verifying block header
00EC1000 : Heap handle
00F10FF8 : Heap block
00000000 : Block size
00000000 :
===================================================
This verifier stop is not continuable. Process will be terminated when you use the 'go' debugger command.
===================================================
Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=6a226447 edx=0012fab7 esi=00f10ff8 edi=00000008
eip=6a22629c esp=0012fb5c ebp=0012fb80 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
6a22629c cc int 3
在这种情况下,该消息告知无法继续此应用程序验证程序停止。 此错误对于进程无法继续运行太严重,应用程序验证程序无法挽救进程。
!avrf 扩展可用于提供有关当前故障的信息:
0:000> !avrf
Global flags: 02000100
Application verifier global flag is set.
Page heap global flag is set.
Application verifier settings (00000001):
- full page heap
Page heaps active in the process (format: pageheap, lightheap, flags):
00941000 , 00a40000 , 3 (pageheap traces )
00b41000 , 00c40000 , 3 (pageheap traces )
00cb1000 , 00db0000 , 3 (pageheap traces )
00ec1000 , 00fc0000 , 3 (pageheap traces )
Current stop 00000008 : 00ec1000 00f10ff8 00000000 00000000 .
Corrupted heap block.
此显示的最后一行总结了问题。
你可能还希望此时查看一些日志。 此时可能需要使用 .restart (重启目标应用程序) 命令。 或者,你可能希望结束应用程序验证程序会话并开始修复代码中的 bug。
调试临界区错误
!cs 调试器扩展
!cs 可用于用户模式调试器和内核调试器,以显示有关当前进程中关键部分的信息。 有关 !cs 扩展的其他信息,请参阅调试器文档中的 !cs 。
需要具有类型信息的匹配符号,尤其是对于 ntdll.dll。
此扩展的语法为:
!cs [-s] - 转储当前进程中的所有活动关键部分。
!cs [-s] 地址 - 在此地址处转储关键部分。
!cs [-s] -d 地址 - 在此地址处转储与 DebugInfo 对应的关键段。
-s 将导出关键节初始化时的堆栈跟踪(如果可用)。
例子:
使用关键节地址导出关于关键节的信息
0:001> ! cs 0x7803B0F8
Critical section = 0x7803B0F8 (MSVCRT!__app_type+0x4)
DebugInfo = 0x6A262080
NOT LOCKED
LockSemaphore = 0x0
SpinCount = 0x0
使用临界区的地址获取信息,包括初始化堆栈跟踪。
0:001> !cs -s 0x7803B0F8
Critical section = 0x7803B0F8 (MSVCRT!__app_type+0x4)
DebugInfo = 0x6A262080
NOT LOCKED
LockSemaphore = 0x0
SpinCount = 0x0
Stack trace for DebugInfo = 0x6A262080:
0x6A2137BD: ntdll!RtlInitializeCriticalSectionAndSpinCount+0x9B
0x6A207A4C: ntdll!LdrpCallInitRoutine+0x14
0x6A205569: ntdll!LdrpRunInitializeRoutines+0x1D9
0x6A20DCE1: ntdll!LdrpInitializeProcess+0xAE5
使用关键节的调试信息地址生成有关关键节的信息
0:001> !cs -d 0x6A262080
DebugInfo = 0x6A262080
Critical section = 0x7803B0F8 (MSVCRT!__app_type+0x4)
NOT LOCKED
LockSemaphore = 0x0
SpinCount = 0x0
使用其调试信息地址转储临界区信息,包括初始化堆栈跟踪
0:001> !cs -s -d 0x6A262080
DebugInfo = 0x6A262080
Critical section = 0x7803B0F8 (MSVCRT!__app_type+0x4)
NOT LOCKED
LockSemaphore = 0x0
SpinCount = 0x0
Stack trace for DebugInfo = 0x6A262080:
0x6A2137BD: ntdll!RtlInitializeCriticalSectionAndSpinCount+0x9B
0x6A207A4C: ntdll!LdrpCallInitRoutine+0x14
0x6A205569: ntdll!LdrpRunInitializeRoutines+0x1D9
0x6A20DCE1: ntdll!LdrpInitializeProcess+0xAE
转储有关当前进程中所有活动关键部分的信息
0:001> !cs
-----------------------------------------
DebugInfo = 0x6A261D60
Critical section = 0x6A262820 (ntdll!RtlCriticalSectionLock+0x0)
LOCKED
LockCount = 0x0
OwningThread = 0x460
RecursionCount = 0x1
LockSemaphore = 0x0
SpinCount = 0x0
-----------------------------------------
DebugInfo = 0x6A261D80
Critical section = 0x6A262580 (ntdll!DeferedCriticalSection+0x0)
NOT LOCKED
LockSemaphore = 0x7FC
SpinCount = 0x0
-----------------------------------------
DebugInfo = 0x6A262600
Critical section = 0x6A26074C (ntdll!LoaderLock+0x0)
NOT LOCKED
LockSemaphore = 0x0
SpinCount = 0x0
.....
转储有关当前进程中所有活动关键部分的信息,包括初始化堆栈跟踪
0:001> !cs -s
...
-----------------------------------------
DebugInfo = 0x6A261EA0
Critical section = 0xA8001C (+0xA8001C)
NOT LOCKED
LockSemaphore = 0x0
SpinCount = 0x0
No stack trace saved
-----------------------------------------
DebugInfo = 0x6A261EC0
Critical section = 0x6A263560 (ntdll!RtlpDphTargetDllsLock+0x0)
NOT LOCKED
LockSemaphore = 0x0
SpinCount = 0x0
No stack trace saved
-----------------------------------------
DebugInfo = 0x6A261EE0
Critical section = 0xA90608 (+0xA90608)
NOT LOCKED
LockSemaphore = 0x7EC
SpinCount = 0x0
Stack trace for DebugInfo = 0x6A261EE0:
0x6A2137BD: ntdll!RtlInitializeCriticalSectionAndSpinCount+0x9B
0x6A20B0DC: ntdll!CsrpConnectToServer+0x1BE
0x6A20B2AA: ntdll!CsrClientConnectToServer+0x148
0x77DBE83F: KERNEL32!BaseDllInitialize+0x11F
0x6A207A4C: ntdll!LdrpCallInitRoutine+0x14
0x6A205569: ntdll!LdrpRunInitializeRoutines+0x1D9
0x6A20DCE1: ntdll!LdrpInitializeProcess+0xAE5
-----------------------------------------
DebugInfo = 0x6A261F00
Critical section = 0x77E1AEB8 (KERNEL32!BaseDllRegistryCache+0x18)
NOT LOCKED
LockSemaphore = 0x0
SpinCount = 0x0
Stack trace for DebugInfo = 0x6A261F00:
0x6A2137BD: ntdll!RtlInitializeCriticalSectionAndSpinCount+0x9B
0x6A207A4C: ntdll!LdrpCallInitRoutine+0x14
0x6A205569: ntdll!LdrpRunInitializeRoutines+0x1D9
0x6A20DCE1: ntdll!LdrpInitializeProcess+0xAE5
调试异常错误
异常日志记录目标进程中发生的所有异常。
可以使用 !avrf -ex Length 扩展命令显示最后几个异常;长度指定异常数。 如果省略 Length,则会显示所有异常。
下面是一个示例:
0:000> !avrf -ex 4
=================================
Thread ID: 0000052c
Exception code: c0000008
Exception address: 6a226663
Exception record: 0012fb50
Context record: 0012fb64
Displayed 1 exception log entries.
调试处理错误
!htrace 可用于用户模式调试器和内核调试器,以显示进程中一个或所有句柄的堆栈跟踪信息。 如果为进程启用了句柄跟踪,则此信息可用 – 如果应用程序验证程序中启用了句柄检查,则会自动启用此信息。 每次进程打开或关闭句柄或引用无效句柄时,都会保存堆栈跟踪。 有关 !htrace 扩展的其他信息,请参阅调试器文档中的 !htrace 。
此扩展的内核调试器语法为:
!htrace [ handle [process] ]
如果未指定句柄或为 0,则将显示有关进程中所有句柄的信息。 如果未指定进程,将使用当前进程。
用户模式调试器语法为:
!htrace [handle]
用户模式调试器扩展始终显示有关当前调试者进程的信息。
例子:
有关处理进程 815328b0 中的 7CC 的转储信息
kd> !htrace 7CC 815328b0
Loaded \\...\kdexts extension DLL
Process 0x815328B0
ObjectTable 0xE15ECBB8
--------------------------------------
Handle 0x7CC - CLOSE:
0x8018FCB9: ntoskrnl!ExDestroyHandle+0x103
0x801E1D12: ntoskrnl!ObpCloseHandleTableEntry+0xE4
0x801E1DD9: ntoskrnl!ObpCloseHandle+0x85
0x801E1EDD: ntoskrnl!NtClose+0x19
0x77DBFCD6: KERNEL32!GetLocaleFileInfo+0x3D
0x77DBF942: KERNEL32!NlsProcessInitialize+0x11D
0x77E0C6DF: KERNEL32!NlsDllInitialize+0x35
0x6A20785C: ntdll!LdrpCallInitRoutine+0x14
0x6A205393: ntdll!LdrpRunInitializeRoutines+0x1D9
0x6A20DD80: ntdll!LdrpInitializeProcess+0xAF6
--------------------------------------
Handle 0x7CC - OPEN:
0x8018F44A: ntoskrnl!ExCreateHandle+0x94
0x801E3180: ntoskrnl!ObpCreateHandle+0x304
0x801E1563: ntoskrnl!ObOpenObjectByName+0x1E9
0x77DBFCD6: KERNEL32!GetLocaleFileInfo+0x3D
0x77DBF942: KERNEL32!NlsProcessInitialize+0x11D
0x77E0C6DF: KERNEL32!NlsDllInitialize+0x35
0x6A20785C: ntdll!LdrpCallInitRoutine+0x14
0x6A205393: ntdll!LdrpRunInitializeRoutines+0x1D9
0x6A20DD80: ntdll!LdrpInitializeProcess+0xAF6
--------------------------------------
Parsed 0x1CA stack traces.
Dumped 0x2 stack traces.
转储有关进程 815328b0 中所有句柄的信息
kd> !htrace 0 81400300
Process 0x81400300
ObjectTable 0xE10CCF60
--------------------------------------
Handle 0x7CC - CLOSE:
0x8018FCB9: ntoskrnl!ExDestroyHandle+0x103
0x801E1D12: ntoskrnl!ObpCloseHandleTableEntry+0xE4
0x801E1DD9: ntoskrnl!ObpCloseHandle+0x85
0x801E1EDD: ntoskrnl!NtClose+0x19
0x010012C1: badhandle!mainCRTStartup+0xE3
0x77DE0B2F: KERNEL32!BaseProcessStart+0x3D
--------------------------------------
Handle 0x7CC - OPEN:
0x8018F44A: ntoskrnl!ExCreateHandle+0x94
0x801E3390: ntoskrnl!ObpCreateUnnamedHandle+0x10C
0x801E7317: ntoskrnl!ObInsertObject+0xC3
0x77DE23B2: KERNEL32!CreateSemaphoreA+0x66
0x010011C5: badhandle!main+0x45
0x010012C1: badhandle!mainCRTStartup+0xE3
0x77DE0B2F: KERNEL32!BaseProcessStart+0x3D
--------------------------------------
Handle 0x7DC - BAD REFERENCE:
0x8018F709: ntoskrnl!ExMapHandleToPointerEx+0xEA
0x801E10F2: ntoskrnl!ObReferenceObjectByHandle+0x12C
0x801902BE: ntoskrnl!NtSetEvent+0x6C
0x80154965: ntoskrnl!_KiSystemService+0xC4
0x010012C1: badhandle!mainCRTStartup+0xE3
0x77DE0B2F: KERNEL32!BaseProcessStart+0x3D
--------------------------------------
Handle 0x7DC - CLOSE:
0x8018FCB9: ntoskrnl!ExDestroyHandle+0x103
0x801E1D12: ntoskrnl!ObpCloseHandleTableEntry+0xE4
0x801E1DD9: ntoskrnl!ObpCloseHandle+0x85
0x801E1EDD: ntoskrnl!NtClose+0x19
0x010012C1: badhandle!mainCRTStartup+0xE3
0x77DE0B2F: KERNEL32!BaseProcessStart+0x3D
--------------------------------------
Handle 0x7DC - OPEN:
0x8018F44A: ntoskrnl!ExCreateHandle+0x94
0x801E3390: ntoskrnl!ObpCreateUnnamedHandle+0x10C
0x801E7317: ntoskrnl!ObInsertObject+0xC3
0x77DE265C: KERNEL32!CreateEventA+0x66
0x010011A0: badhandle!main+0x20
0x010012C1: badhandle!mainCRTStartup+0xE3
0x77DE0B2F: KERNEL32!BaseProcessStart+0x3D
--------------------------------------
Parsed 0x6 stack traces.
Dumped 0x5 stack traces.
转储当前进程中句柄 7DC 的信息
kd> !htrace 7DC
Process 0x81400300
ObjectTable 0xE10CCF60
--------------------------------------
Handle 0x7DC - BAD REFERENCE:
0x8018F709: ntoskrnl!ExMapHandleToPointerEx+0xEA
0x801E10F2: ntoskrnl!ObReferenceObjectByHandle+0x12C
0x801902BE: ntoskrnl!NtSetEvent+0x6C
0x80154965: ntoskrnl!_KiSystemService+0xC4
0x010012C1: badhandle!mainCRTStartup+0xE3
0x77DE0B2F: KERNEL32!BaseProcessStart+0x3D
--------------------------------------
Handle 0x7DC - CLOSE:
0x8018FCB9: ntoskrnl!ExDestroyHandle+0x103
0x801E1D12: ntoskrnl!ObpCloseHandleTableEntry+0xE4
0x801E1DD9: ntoskrnl!ObpCloseHandle+0x85
0x801E1EDD: ntoskrnl!NtClose+0x19
0x010012C1: badhandle!mainCRTStartup+0xE3
0x77DE0B2F: KERNEL32!BaseProcessStart+0x3D
--------------------------------------
Handle 0x7DC - OPEN:
0x8018F44A: ntoskrnl!ExCreateHandle+0x94
0x801E3390: ntoskrnl!ObpCreateUnnamedHandle+0x10C
0x801E7317: ntoskrnl!ObInsertObject+0xC3
0x77DE265C: KERNEL32!CreateEventA+0x66
0x010011A0: badhandle!main+0x20
0x010012C1: badhandle!mainCRTStartup+0xE3
0x77DE0B2F: KERNEL32!BaseProcessStart+0x3D
--------------------------------------
Parsed 0x6 stack traces.
Dumped 0x3 stack traces.
调试堆错误
堆验证程序调试器扩展
堆验证程序调试器扩展是 !堆扩展(NT 堆调试器扩展)的一部分。 可以使用 !heap 获取简单的帮助 -? 或使用 !heap -p -? 进行更详细的分析 . 当前扩展程序无法自行检测进程是否启用了页面堆,因此无法相应采取行动。 现在,扩展的用户需要知道页面堆已启用,并使用以 !heap -p 为前缀的命令。 有关 !htrace 扩展的其他信息,请参阅调试器文档中的 !heap。
!heap -p
转储进程中创建的所有整页堆的地址。
!heap -p -h ADDRESS-OF-HEAP
在 ADDRESS-OF-HEAP 处完整转储整页堆。
!heap -p -a ADDRESS
尝试找出 ADDRESS 中是否存在堆块。 此值不必是块的起始地址。 如果对内存区域的性质没有任何线索,则此命令非常有用。
堆操作日志
堆操作日志跟踪所有堆操作。 其中包括 HeapAlloc、HeapReAlloc 和 HeapFree。
可以使用 !avrf -hp Length 扩展命令显示最后几个记录;长度指定记录数。
您可以使用 !avrf -hp -a Address 显示影响指定地址的所有堆空间操作。 对于分配操作,只需地址包含在分配的堆块内即可。 对于免费操作,必须提供块的起始地址。
对于日志中的每个条目,将显示以下信息:
- 调用了堆函数。
- 调用例程的线程的线程 ID。
- 调用中涉及的内存地址 - 这是分配例程返回的或传递到释放例程的地址。
- 调用中涉及的区域的大小。
- 调用的堆栈跟踪。
最新条目首先显示。
在此示例中,将显示两个最新的条目:
0:001> !avrf -hp 2
alloc (tid: 0xFF4):
address: 00ea2fd0
size: 00001030
00403062: Prymes!_heap_alloc_dbg+0x1A2
00402e69: Prymes!_nh_malloc_dbg+0x19
00402e1e: Prymes!_malloc_dbg+0x1E
00404ff3: Prymes!_stbuf+0xC3
00401c23: Prymes!printf+0x43
00401109: Prymes!main+0xC9
00402039: Prymes!mainCRTStartup+0xE9
77e7a278: kernel32!BaseProcessStart+0x23
alloc (tid: 0xFF4):
address: 00ea07d0
size: 00000830
00403062: Prymes!_heap_alloc_dbg+0x1A2
00402e69: Prymes!_nh_malloc_dbg+0x19
00402e1e: Prymes!_malloc_dbg+0x1E
00403225: Prymes!_calloc_dbg+0x25
00401ad5: Prymes!__initstdio+0x45
00401f38: Prymes!_initterm+0x18
00401da1: Prymes!_cinit+0x21
00402014: Prymes!mainCRTStartup+0xC4
77e7a278: kernel32!BaseProcessStart+0x23
典型的调试场景
可能会遇到几种故障方案。 其中一些任务需要相当多的调查工作才能了解全貌。
不可访问页面中的访问违规
如果测试的应用程序访问超出缓冲区末尾的访问权限,则会在启用完整页堆时发生这种情况。 如果它接触到已释放的内存块,也可能会发生这种情况。 若要了解发生异常的地址的性质,需要使用:
!heap –p –a ADDRESS-OF-AV
损坏的阻止消息
在分配(分配、用户释放、实际释放)的生命周期内,页面堆管理器会检查块的所有填充模式是否完好无损,并且块标头的数据是否一致。 如果不是这种情况,验证器将停止。
如果块是完整页面堆块(例如,如果您明确知道已为所有分配启用了完整页面堆),则可以使用“!heap –p –a 地址”来了解块的特征。
如果块是轻页堆块,则需要确定块标头的起始地址。 可以通过在报告地址下方转储 30-40 字节来查找起始地址,并查找块标头的神奇开始/结束模式(ABCDAAAA、ABCDBBBB、ABCDAAA9、ABCDBBBA)。
标头将提供了解失败所需的所有信息。 特别是,魔法模式会告知该块是否已分配还是空闲,以及它是浅页堆的块还是整页堆的块。 此处的信息必须与涉及问题的呼叫进行仔细核对。
例如,如果对 HeapFree 的调用是使用块地址加上四个字节进行的,则会收到损坏的消息。 块标头看起来很好,但你必须注意到标头结束后(0xDCBAXXXX 魔术值后)的第一个字节具有与调用中不同的地址。
特殊填充指针
页面堆管理器用看起来像内核指针的值填充用户的内存分配。 当块被释放时(填充值为 F0),以及当块被分配但没有请求将块清零时(对于浅页堆,填充值为 E0,对于整页堆,填充值为 C0)。 非零分配是 malloc/new 用户的典型分配。 如果出现故障(访问冲突),在F0F0F0F0、E0E0E0E0、C0C0C0C0等地址尝试读取/写入时,很可能是遇到这种情况。
F0F0F0F0处的读/写操作意味着释放后的内存块再次被使用。 遗憾的是,你需要进行一些调查研究来找出导致此问题的模块。 需要获取失败的堆栈跟踪,然后检查堆栈上函数的代码。 其中一个可能会对内存分配仍然存在做出错误的假设。
E0E0E0E0/C0C0C0C0 处的读/写意味着应用程序未正确初始化分配。 这还需要对当前堆栈跟踪中的函数进行代码检查。 下面是此类故障的示例。 在测试过程中,注意到在地址 E0E0E0E0 上执行 HeapFree 时发生访问冲突。 事实证明,测试分配了一个结构体,未对其进行正确初始化,然后调用了结构体的析构函数。 由于某个字段不为 null(其值为E0E0E0E0),因此对其调用了删除操作。
页面堆技术详细信息
若要检测堆损坏(溢出或下溢),AppVerifier 将修改内存分配方式,方法是使用完整不可写页面填充请求的内存,或在分配的内存前后使用特殊标记填充内存。 AppVerifier 通过将 Verifier.dll 加载到正在验证的进程中,并将应用程序调用的一些 Win32 堆 API 重定向至相应的 Verifier.dll API 来实现其功能。
使用完整不可写页面填充请求的内存(在页面堆属性部分启用 FULL 设置并且是默认设置)时,AppVerifier 将使用大量的虚拟内存,但具有在发生溢出或下溢时实时缓存堆损坏事件的优势。 请记住,此模式下的内存将如下所示[AppVerifier Read-Only 堆页 (4k)] [受测试应用程序请求的内存量]或类似于 [受测试应用程序请求的内存量] [AppVerifier Read-Only 堆页 (4k)]。
堆检查将在分配的开头或末尾放置一个保护页,具体取决于 Backward 属性。 如果“Backward”设置为 False(默认值),它将在分配的末尾放置一个防护页,以捕获缓冲区溢出。 如果将其设置为 True,防护页将被放置在分配的开头,以捕获缓冲区下溢。
使用特殊标记填充请求的内存时(通过在堆属性中清除“完整”复选框项启用),AppVerifier 将在释放此内存时检查并发出警报。 使用此技术的主要问题是,在某些情况下,只有在释放内存时才会检测到内存损坏(最小内存块为 8 个字节),因此在 3 字节变量或 5 字节溢出时,不会立即检测到内存损坏。
在发生下溢事件时,将尝试写入 Read-Only 页面。 这会触发异常。 请注意,仅当目标应用程序正在调试器下执行时,才能捕获此异常。 请注意,完整页面堆积模式也会检测这些错误,因为它使用填充+防护页。 使用浅页堆的原因是,如果计算机无法容忍全页堆的高内存限制。
对于内存密集型应用程序,或者在长时间(例如压力测试)中使用 AppVerifier 时,最好运行正常(轻型)堆测试,而不是运行完全模式,因为后者会导致性能下降。 但是,遇到问题时,请打开完整页堆以进一步调查。
使用自定义堆的应用程序(绕过操作系统实现堆的堆)可能无法充分利用页面堆,甚至可能在启用页面堆时发生故障。
调试内存错误
内存验证程序调试器扩展
虚拟空间操作日志跟踪所有以任何方式修改进程虚拟空间的例程。 其中包括 VirtualAlloc、VirtualFree、MapViewOfFile 和 UnmapViewOfFile。
可以使用 !avrf -vs Length 扩展命令显示最后几个记录;长度指定记录数。
可以使用 !avrf -vs -a Address 来显示影响指定地址的所有虚拟空间操作。 对于分配而言,只要地址位于分配块内即可。 对于免费地址,必须提供该区域开头的确切地址。
对于日志中的每个条目,将显示以下信息:
- 被调用的函数
- 例程被调用的线程 ID
- 调用中涉及的地址 - 这是分配例程返回的地址,或者传递给免费例程的地址
- 调用中涉及的区域的大小
- 内存操作的类型(AllocationType 参数)
- 请求的保护类型
- 调用的堆栈追踪
例子
最新条目首先显示。
在以下示例中,将显示两个最新的条目:
0:001> !avrf -vs 2
VirtualFree (tid: 0xB4): addr:04bb0000 sz:00400000 op:8000 prot:0
00aa1ac2: verifier!VsLogCall+0x42
00aa19c1: verifier!AVrfpNtFreeVirtualMemory+0x30
68925d17: kernel32!VirtualFreeEx+0x35
6892611c: kernel32!VirtualFree+0x13
75ef6525: mshtml+0x116525
75ef68af: mshtml+0x1168AF
6a20787c: ntdll!LdrpCallInitRoutine+0x14
6a211c6f: ntdll!LdrUnloadDll+0x39A
689275c1: kernel32!FreeLibrary+0x3B
77b22d69: ole32!CoQueryReleaseObject+0x1E6
77b02bd2: ole32!SetErrorInfo+0x1ED
VirtualFree (tid: 0xB4): addr:04bb0000 sz:00001000 op:4000 prot:0
00aa1ac2: verifier!VsLogCall+0x42
00aa19c1: verifier!AVrfpNtFreeVirtualMemory+0x30
68925d17: kernel32!VirtualFreeEx+0x35
6892611c: kernel32!VirtualFree+0x13
75ef65ae: mshtml+0x1165AE
75ef68af: mshtml+0x1168AF
6a20787c: ntdll!LdrpCallInitRoutine+0x14
6a211c6f: ntdll!LdrUnloadDll+0x39A
689275c1: kernel32!FreeLibrary+0x3B
77b22d69: ole32!CoQueryReleaseObject+0x1E6
77b02bd2: ole32!SetErrorInfo+0x1ED
从输出中可以看到线程0xB4先取消提交页面,然后释放整个虚拟区域。
以下是影响地址0x4BB1000的所有操作:
0:001> !avrf -vs -a 4bb1000
Searching in vspace log for address 04bb1000 ...
VirtualFree (tid: 0xB4): addr:04bb0000 sz:00400000 op:8000 prot:0
00aa1ac2: verifier!VsLogCall+0x42
00aa19c1: verifier!AVrfpNtFreeVirtualMemory+0x30
68925d17: kernel32!VirtualFreeEx+0x35
6892611c: kernel32!VirtualFree+0x13
75ef6525: mshtml+0x116525
75ef68af: mshtml+0x1168AF
6a20787c: ntdll!LdrpCallInitRoutine+0x14
6a211c6f: ntdll!LdrUnloadDll+0x39A
689275c1: kernel32!FreeLibrary+0x3B
77b22d69: ole32!CoQueryReleaseObject+0x1E6
77b02bd2: ole32!SetErrorInfo+0x1ED
VirtualFree (tid: 0xB4): addr:04bb1000 sz:00001000 op:4000 prot:0
00aa1ac2: verifier!VsLogCall+0x42
00aa19c1: verifier!AVrfpNtFreeVirtualMemory+0x30
68925d17: kernel32!VirtualFreeEx+0x35
6892611c: kernel32!VirtualFree+0x13
75ef65ae: mshtml+0x1165AE
75ef68af: mshtml+0x1168AF
6a20787c: ntdll!LdrpCallInitRoutine+0x14
6a211c6f: ntdll!LdrUnloadDll+0x39A
689275c1: kernel32!FreeLibrary+0x3B
77b22d69: ole32!CoQueryReleaseObject+0x1E6
77b02bd2: ole32!SetErrorInfo+0x1ED
VirtualAlloc (tid: 0xB4): addr:04bb0000 sz:00010000 op:1000 prot:4
00aa1ac2: verifier!VsLogCall+0x42
00aa1988: verifier!AVrfpNtAllocateVirtualMemory+0x37
68925ca3: kernel32!VirtualAllocEx+0x61
68926105: kernel32!VirtualAlloc+0x16
75ef63f3: mshtml+0x1163F3
VirtualAlloc (tid: 0xB4): addr:04bb0000 sz:00400000 op:2000 prot:4
00aa1ac2: verifier!VsLogCall+0x42
00aa1988: verifier!AVrfpNtAllocateVirtualMemory+0x37
68925ca3: kernel32!VirtualAllocEx+0x61
68926105: kernel32!VirtualAlloc+0x16
75ef63d9: mshtml+0x1163D9
若要读取此输出,请注意,条目是从最新的开始列出的。 因此,此日志显示线程 0xB4 分配了一个大型区域,并在其中提交了一个页面。 稍后将取消提交页面,然后释放整个虚拟区域。