共用方式為


偵錯 Stack Overflow

堆疊溢位是使用者模式執行緒可能會遇到的錯誤。 此錯誤有三個可能的原因:

  • 執行緒會使用保留給它的整個堆疊。 這通常是由無限遞迴所造成。

  • 執行緒無法擴充堆疊,因為頁面檔案已上限,因此無法認可其他頁面來擴充堆疊。

  • 執行緒無法擴充堆疊,因為系統在用來擴充頁面檔案的簡短期間內。

當執行緒上執行的函式配置區域變數時,變數會放線上程的呼叫堆疊上。 函式所需的堆疊空間數量可能會與所有區域變數的大小總和相同。 不過,編譯器通常會執行優化,以減少函式所需的堆疊空間。 例如,如果兩個變數位於不同的範圍內,編譯器就可以針對這兩個變數使用相同的堆疊記憶體。 編譯器也可以藉由優化計算來完全消除某些區域變數。

優化數量會受到在建置時間套用的編譯器設定所影響。 例如,由 /F (設定堆疊大小) - C++ 編譯器選項

本主題假設概念的一般知識,例如執行緒、執行緒區塊、堆疊和堆積。 如需這些基本概念的其他資訊,請參閱 Mark Russinovich 和 David Foundation 的 Microsoft Windows 內部

在沒有符號的情況下偵錯堆疊溢位

以下是如何偵錯堆疊溢位的範例。 在此範例中,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。 (堆疊會在 memory 中向下成長。) 您可以使用 ? (Evaluate Expression) 命令來計算堆疊大小:

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,這表示已保留足夠的堆疊空間。

此外,此程式看起來很乾淨-- 它不在無限的遞迴中,或使用過多的堆疊式資料結構超過其堆疊空間。

現在會分成 KD,並使用 !vm 擴充功能命令查看整體系統記憶體使用量:

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。

當符號可用時偵錯堆疊溢位

符號會提供標籤給儲存在記憶體中的專案,而且當可用時,可以更輕鬆地檢查程式碼。 如需符號的概觀,請參閱 使用符號。 如需設定符號路徑的詳細資訊,請參閱 . (設定符號路徑)

若要建立堆疊溢位,我們可以使用此程式碼,它會繼續呼叫副程式,直到堆疊耗盡為止。

// 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

命令摘要

另請參閱

使用 WinDbg (使用者模式) 消費者入門

/F (設定堆疊大小) - C++ 編譯器選項