共用方式為


偵錯 Stack Overflow

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

  • 線程會使用保留給它的整個堆疊。 這通常是由無限遞歸所造成。

  • 線程無法擴充堆疊,因為頁面檔案已最大化,因此無法認可其他頁面來擴充堆疊。

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

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

優化的數量會受到建置階段所套用的編譯程式設定所影響。 例如,依 /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 內部

查看 32 位版本的 _TEB 結構,指出 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,這表示剩餘的堆疊空間超過足夠的空間。

此外,此程式看起來很乾淨-- 它不是無限遞歸或超過其堆疊空間,方法是使用過多的堆疊型數據結構。

現在分成 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)
         ..... 

首先,查看未分頁和分頁集區使用量。 兩者都處於極限範圍內,因此這不是問題的原因。

接下來,查看 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

我們也可以使用 顯示 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++編譯程序選項