调试堆栈溢出

堆栈溢出是用户模式线程可能会遇到的错误。 此错误有三个可能的原因:

  • 线程使用为其保留的整个堆栈。 这通常是由无限递归引起的。

  • 线程无法扩展堆栈,因为页面文件已最大化,因此无法提交其他页面来扩展堆栈。

  • 线程无法扩展堆栈,因为系统在用于扩展页面文件的短时间内。

当线程上运行的函数分配局部变量时,变量将放在线程的调用堆栈上。 函数所需的堆栈空间量可以与所有局部变量的大小之和一样大。 但是,编译器通常执行优化来减少函数所需的堆栈空间。 例如,如果两个变量位于不同的范围内,则编译器可以为这两个变量使用相同的堆栈内存。 编译器还可以通过优化计算来完全消除某些局部变量。

优化量受生成时应用的编译器设置的影响。 例如,通过 /F (设置堆栈大小) - C++ 编译器选项

本主题假定对线程、线程块、堆栈和堆等概念有一般了解。 有关这些基本概念的其他信息,请参阅 Microsoft Windows Internals by Mark Russinovich 和 David Solomon。

调试没有符号的堆栈溢出

下面是如何调试堆栈溢出的示例。 在此示例中,NTSD 与目标应用程序在同一台计算机上运行,并且将其输出重定向到主计算机上的 KD。 有关详细信息 ,请参阅从内核调试器控制 User-Mode 调试器

第一步是查看导致调试器中断的事件:

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 (Display Memory) 命令在该位置显示原始值:

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 内部

查看 32 位版本的 _TEB 结构,它指示 TEB 结构中的第二个和第三个 DWORD 分别指向堆栈的底部和顶部。 在此示例中,这些地址0x00A00000和0x009FC000。 (堆栈在内存中向下增长。) 可以使用 ? (计算表达式) 命令计算堆栈大小:

0:002> ? a00000-9fc000
Evaluate expression: 16384 = 00004000 

这表明堆栈大小为 16 K。最大堆栈大小存储在 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)
         ..... 

首先,查看非分页和分页池的使用情况。 两者都在限制范围内,因此这些不是问题的原因。

接下来,查看提交的页数:202280 中的183528。 这非常接近限制。 虽然此显示器未显示此数字完全达到限制,但应记住,在执行用户模式调试时,其他进程正在系统上运行。 每次执行 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

我们也可以使用 !teb 命令来显示 StackBase abd StackLimit。

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

命令摘要

另请参阅

Getting Started with WinDbg (User-Mode)(WinDbg 入门(用户模式))

/F (设置堆栈大小) - C++ 编译器选项