调试堆栈溢出
堆栈溢出是用户模式线程可能会遇到的错误。 此错误有三个可能的原因:
线程使用为其保留的整个堆栈。 这通常是由无限递归引起的。
线程无法扩展堆栈,因为页面文件已最大化,因此无法提交其他页面来扩展堆栈。
线程无法扩展堆栈,因为系统在用于扩展页面文件的短时间内。
当线程上运行的函数分配局部变量时,变量将放在线程的调用堆栈上。 函数所需的堆栈空间量可以和所有局部变量的大小之和一样大。 但是,编译器通常会执行优化,以减少函数所需的堆栈空间。 例如,如果两个变量位于不同的范围内,则编译器可以为这两个变量使用相同的堆栈内存。 编译器还可以通过优化计算来完全消除某些局部变量。
优化量受生成时应用的编译器设置的影响。 例如,按 /F (设置堆栈大小) - C++编译器选项。
本主题假定大致了解线程、线程块、堆栈和堆等概念。 有关这些基本概念的其他信息,请参阅 Mark Russinovich 和 David 所罗门Microsoft Windows 内部 。
在没有符号的情况下调试堆栈溢出
下面是有关如何调试堆栈溢出的示例。 在此示例中,NTSD 与目标应用程序在同一台计算机上运行,并将输出重定向到主计算机上的 KD。 有关详细信息,请参阅从内核调试器控制用户模式调试器。
第一步是查看导致调试器中断的事件:
0:002> .lastevent
Last event: Exception C00000FD, second chance
可以在 ntstatus.h 中查找异常代码0xC00000FD,此异常代码STATUS_STACK_OVERFLOW,这表示无法创建堆栈的新防护页。所有状态代码都列在 2.3.1 NTSTATUS 值中。
还可以使用 !error 命令在 Windows 调试器中查找错误。
0:002> !error 0xC00000FD
Error code: (NTSTATUS) 0xc00000fd (3221225725) - A new guard page for the stack cannot be created.
若要仔细检查堆栈是否溢出,可以使用 k(显示堆栈回溯) 命令:
0:002> k
ChildEBP RetAddr
009fdd0c 71a32520 COMCTL32!_chkstk+0x25
009fde78 77cf8290 COMCTL32!ListView_WndProc+0x4c4
009fde98 77cfd634 USER32!_InternalCallWinProc+0x18
009fdf00 77cd55e9 USER32!UserCallWinProcCheckWow+0x17f
009fdf3c 77cd63b2 USER32!SendMessageWorker+0x4a3
009fdf5c 71a45b30 USER32!SendMessageW+0x44
009fdfec 71a45bb0 COMCTL32!CCSendNotify+0xc0e
009fdffc 71a1d688 COMCTL32!CICustomDrawNotify+0x2a
009fe074 71a1db30 COMCTL32!Header_Draw+0x63
009fe0d0 71a1f196 COMCTL32!Header_OnPaint+0x3f
009fe128 77cf8290 COMCTL32!Header_WndProc+0x4e2
009fe148 77cfd634 USER32!_InternalCallWinProc+0x18
009fe1b0 77cd4490 USER32!UserCallWinProcCheckWow+0x17f
009fe1d8 77cd46c8 USER32!DispatchClientMessage+0x31
009fe200 77f7bb3f USER32!__fnDWORD+0x22
009fe220 77cd445e ntdll!_KiUserCallbackDispatcher+0x13
009fe27c 77cfd634 USER32!DispatchMessageWorker+0x3bc
009fe2e4 009fe4a8 USER32!UserCallWinProcCheckWow+0x17f
00000000 00000000 0x9fe4a8
目标线程已分解为 COMCTL32!_chkstk,这表示堆栈问题。 现在,应调查目标进程的堆栈使用情况。 进程有多个线程,但重要的是导致溢出的线程,因此首先使用 ~ (线程状态) 命令标识此线程:
0:002> ~*k
0 id: 570.574 Suspend: 1 Teb 7ffde000 Unfrozen
.....
1 id: 570.590 Suspend: 1 Teb 7ffdd000 Unfrozen
.....
. 2 id: 570.598 Suspend: 1 Teb 7ffdc000 Unfrozen
ChildEBP RetAddr
009fdd0c 71a32520 COMCTL32!_chkstk+0x25
.....
3 id: 570.760 Suspend: 1 Teb 7ffdb000 Unfrozen
现在需要调查线程 2。 此行左侧的句点指示这是当前线程。
堆栈信息包含在0x7FFDC000的 TEB (线程环境块) 中。 列出它的最简单方法是使用 !teb。
0:000> !teb
TEB at 000000c64b95d000
ExceptionList: 0000000000000000
StackBase: 000000c64ba80000
StackLimit: 000000c64ba6f000
SubSystemTib: 0000000000000000
FiberData: 0000000000001e00
ArbitraryUserPointer: 0000000000000000
Self: 000000c64b95d000
EnvironmentPointer: 0000000000000000
ClientId: 0000000000003bbc . 0000000000004ba0
RpcHandle: 0000000000000000
Tls Storage: 0000027957243530
PEB Address: 000000c64b95c000
LastErrorValue: 0
LastStatusValue: 0
Count Owned Locks: 0
HardErrorMode: 0```
但是,这要求你具有正确的符号。 更困难的情况是,没有符号,需要使用 dd (显示内存) 命令在该位置显示原始值:
0:002> dd 7ffdc000 L4
7ffdc000 009fdef0 00a00000 009fc000 00000000
若要解释这一点,需要查找 TEB 数据结构的定义。 使用 dt Display Type 命令在符号可用的系统上执行此操作。
0:000> dt _TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
+0x034 LastErrorValue : Uint4B
+0x038 CountOfOwnedCriticalSections : Uint4B
+0x03c CsrClientThread : Ptr32 Void
+0x040 Win32ThreadInfo : Ptr32 Void
+0x044 User32Reserved : [26] Uint4B
+0x0ac UserReserved : [5] Uint4B
+0x0c0 WOW32Reserved : Ptr32 Void
...
线程数据结构
若要了解有关线程的详细信息,还可以显示有关线程控制块相关结构的信息 ethread 和 kthread。 (请注意,此处显示了 64 位示例。
0:001> dt nt!_ethread
ntdll!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x430 CreateTime : _LARGE_INTEGER
+0x438 ExitTime : _LARGE_INTEGER
+0x438 KeyedWaitChain : _LIST_ENTRY
+0x448 PostBlockList : _LIST_ENTRY
+0x448 ForwardLinkShadow : Ptr64 Void
+0x450 StartAddress : Ptr64 Void
...
0:001> dt nt!_kthread
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x018 SListFaultAddress : Ptr64 Void
+0x020 QuantumTarget : Uint8B
+0x028 InitialStack : Ptr64 Void
+0x030 StackLimit : Ptr64 Void
+0x038 StackBase : Ptr64 Void
有关 线程数据结构的详细信息,请参阅 Microsoft Windows 内部 。
查看_TEB结构的 32 位版本,它指示 TEB 结构中的第二和第三个 DWORD 分别指向堆栈的底部和顶部。 在此示例中,这些地址是0x00A00000和0x009FC000。 (堆栈在内存中向下增长。可以使用 ?(计算表达式) 命令:
0:002> ? a00000-9fc000
Evaluate expression: 16384 = 00004000
这表明堆栈大小为 16 K。最大堆栈大小存储在字段 DeallocationStack 中,这是此 TEB 结构的一部分。 该 DeallocationStack
字段指示堆栈的基数。 进行一些计算后,可以确定此字段的偏移量0xE0C。
0:002> dd 7ffdc000+e0c L1
7ffdce0c 009c0000
0:002> ? a00000-9c0000
Evaluate expression: 262144 = 00040000
这表明最大堆栈大小为 256 K,这意味着剩余的堆栈空间超过足够的堆栈空间。
此外,此过程看起来很干净 -- 它不是无限递归,也不是通过使用过多的基于堆栈的数据结构超过其堆栈空间。
现在,使用 !vm 扩展命令了解整个系统内存使用情况,了解 KD:
0:002> .breakin
Break instruction exception - code 80000003 (first chance)
ntoskrnl!_DbgBreakPointWithStatus+4:
80148f9c cc int 3
kd> !vm
*** Virtual Memory Usage ***
Physical Memory: 16268 ( 65072 Kb)
Page File: \??\C:\pagefile.sys
Current: 147456Kb Free Space: 65988Kb
Minimum: 98304Kb Maximum: 196608Kb
Available Pages: 2299 ( 9196 Kb)
ResAvail Pages: 4579 ( 18316 Kb)
Locked IO Pages: 93 ( 372 Kb)
Free System PTEs: 42754 ( 171016 Kb)
Free NP PTEs: 5402 ( 21608 Kb)
Free Special NP: 348 ( 1392 Kb)
Modified Pages: 757 ( 3028 Kb)
NonPagedPool Usage: 811 ( 3244 Kb)
NonPagedPool Max: 6252 ( 25008 Kb)
PagedPool 0 Usage: 1337 ( 5348 Kb)
PagedPool 1 Usage: 893 ( 3572 Kb)
PagedPool 2 Usage: 362 ( 1448 Kb)
PagedPool Usage: 2592 ( 10368 Kb)
PagedPool Maximum: 13312 ( 53248 Kb)
Shared Commit: 3928 ( 15712 Kb)
Special Pool: 1040 ( 4160 Kb)
Shared Process: 3641 ( 14564 Kb)
PagedPool Commit: 2592 ( 10368 Kb)
Driver Commit: 887 ( 3548 Kb)
Committed pages: 45882 ( 183528 Kb)
Commit limit: 50570 ( 202280 Kb)
Total Private: 33309 ( 133236 Kb)
.....
首先,查看未分页的池使用情况和分页池使用情况。 两者都在限制范围内,因此这不是问题的原因。
接下来,查看提交的页数:183528 202280 年。 这非常接近限制。 虽然此显示未显示此数字完全处于限制,但请记住,在执行用户模式调试时,系统上运行其他进程。 每次执行 NTSD 命令时,这些其他进程也会分配和释放内存。 这意味着,在堆栈溢出发生时,你不知道内存状态到底是什么样子。 鉴于提交的页码与限制的接近程度,因此可以合理地得出结论,页面文件在某个时间点使用,这会导致堆栈溢出。
这种情况并不常见,目标应用程序对此无法真正出错。 如果频繁发生,可能需要考虑为失败的应用程序提高初始堆栈承诺。
分析单个函数调用
了解特定函数调用分配的堆栈空间量也很有用。
为此,请反汇编前几个说明并查找指令 sub esp
编号。 这会移动堆栈指针,从而有效地保留 本地数据的字节数 。
以下是一个示例。 首先使用 k 命令查看堆栈。
0:002> k
ChildEBP RetAddr
009fdd0c 71a32520 COMCTL32!_chkstk+0x25
009fde78 77cf8290 COMCTL32!ListView_WndProc+0x4c4
009fde98 77cfd634 USER32!_InternalCallWinProc+0x18
009fdf00 77cd55e9 USER32!UserCallWinProcCheckWow+0x17f
009fdf3c 77cd63b2 USER32!SendMessageWorker+0x4a3
009fdf5c 71a45b30 USER32!SendMessageW+0x44
009fdfec 71a45bb0 COMCTL32!CCSendNotify+0xc0e
009fdffc 71a1d688 COMCTL32!CICustomDrawNotify+0x2a
009fe074 71a1db30 COMCTL32!Header_Draw+0x63
009fe0d0 71a1f196 COMCTL32!Header_OnPaint+0x3f
009fe128 77cf8290 COMCTL32!Header_WndProc+0x4e2
然后使用 u、ub、uu (Unassemble) 命令查看该地址的汇编程序代码。
0:002> u COMCTL32!Header_Draw
COMCTL32!Header_Draw :
71a1d625 55 push ebp
71a1d626 8bec mov ebp,esp
71a1d628 83ec58 sub esp,0x58
71a1d62b 53 push ebx
71a1d62c 8b5d08 mov ebx,[ebp+0x8]
71a1d62f 56 push esi
71a1d630 57 push edi
71a1d631 33f6 xor esi,esi
这表明Header_Draw分配的堆栈空间0x58字节。
r (Registers) 命令提供有关寄存器的当前内容的信息,例如 esp。
当符号可用时调试堆栈溢出
符号为内存中存储的项提供标签,并且当可用时,可以更轻松地检查代码。 有关符号的概述,请参阅 “使用符号”。 有关设置符号路径的信息,请参阅 .sympath (设置符号路径)。
若要创建堆栈溢出,可以使用此代码,该代码会继续调用子例程,直到堆栈耗尽为止。
// StackOverFlow1.cpp
// This program calls a sub routine using recursion too many times
// This causes a stack overflow
//
#include <iostream>
void Loop2Big()
{
const char* pszTest = "My Test String";
for (int LoopCount = 0; LoopCount < 10000000; LoopCount++)
{
std::cout << "In big loop \n";
std::cout << (pszTest), "\n";
std::cout << "\n";
Loop2Big();
}
}
int main()
{
std::cout << "Calling Loop to use memory \n";
Loop2Big();
}
在 WinDbg 下编译并运行代码时,它将循环一些次数,然后引发堆栈溢出异常。
(336c.264c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0fa90000 edx=00000000 esi=773f1ff4 edi=773f25bc
eip=77491a02 esp=010ffa0c ebp=010ffa38 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2b:
77491a02 cc int 3
0:000> g
(336c.264c): Stack overflow - code c00000fd (first chance)
使用 !analyze 命令检查循环是否确实有问题。
...
FAULTING_SOURCE_LINE_NUMBER: 25
FAULTING_SOURCE_CODE:
21: int main()
22: {
23: std::cout << "Calling Loop to use memory \n";
24: Loop2Big();
> 25: }
26:
使用 kb 命令,我们看到循环程序有多个实例,每个实例都使用内存。
0:000> kb
# ChildEBP RetAddr Args to Child
...
0e 010049b0 00d855b5 01004b88 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x57 [C:\StackOverFlow1\StackOverFlow1.cpp @ 13]
0f 01004a9c 00d855b5 01004c74 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
10 01004b88 00d855b5 01004d60 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
11 01004c74 00d855b5 01004e4c 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
12 01004d60 00d855b5 01004f38 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
13 01004e4c 00d855b5 01005024 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
14 01004f38 00d855b5 01005110 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
15 01005024 00d855b5 010051fc 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
16 01005110 00d855b5 010052e8 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
17 010051fc 00d855b5 010053d4 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
18 010052e8 00d855b5 010054c0 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
19 010053d4 00d855b5 010055ac 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
1a 010054c0 00d855b5 01005698 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
1b 010055ac 00d855b5 01005784 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17]
...
如果符号可用, dt _TEB 可用于显示有关线程块的信息。 有关线程内存的详细信息,请参阅 线程堆栈大小。
0:000> dt _TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
+0x034 LastErrorValue : Uint4B
+0x038 CountOfOwnedCriticalSections : Uint4B
+0x03c CsrClientThread : Ptr32 Void
+0x040 Win32ThreadInfo : Ptr32 Void
+0x044 User32Reserved : [26] Uint4B
+0x0ac UserReserved : [5] Uint4B
+0x0c0 WOW32Reserved : Ptr32 Void
我们还可以使用 显示 StackBase abd StackLimit 的 !teb 命令。
0:000> !teb
TEB at 00ff8000
ExceptionList: 01004570
StackBase: 01100000
StackLimit: 01001000
SubSystemTib: 00000000
FiberData: 00001e00
ArbitraryUserPointer: 00000000
Self: 00ff8000
EnvironmentPointer: 00000000
ClientId: 0000336c . 0000264c
RpcHandle: 00000000
Tls Storage: 00ff802c
PEB Address: 00ff5000
LastErrorValue: 0
LastStatusValue: c00700bb
Count Owned Locks: 0
HardErrorMode: 0
可以使用此命令计算堆栈大小。
0:000> ?? int(@$teb->NtTib.StackBase) - int(@$teb->NtTib.StackLimit)
int 0n1044480
命令摘要
- k(显示堆栈回溯)
- ~(线程状态)
- d、da、db、dc、dd、dD、df、dp、dq、du、dw(显示内存)
- u、ub、uu (Unassemble)
- r(寄存器)
- .sympath(设置符号路径)
- x(检查符号)
- dt (显示类型)
- !analyze
- !teb
另请参阅
Getting Started with WinDbg (User-Mode)(WinDbg 入门(用户模式))