偵錯 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
命令摘要
- k (顯示堆疊回溯)
- ~ (線程狀態)
- d, da, db, dc, dd, dD, df, dp, dq, du, dw (顯示記憶體)
- u、 ub、 uu (Unassemble)
- r (快取器)
- .sympath (設定符號路徑)
- x (檢查符號)
- dt (顯示類型)
- !分析
- !teb