Debuggen eines Stapelüberlaufs
Ein Stapelüberlauf ist ein Fehler, auf den Benutzermodusthreads stoßen können. Für diesen Fehler gibt es drei mögliche Ursachen:
Ein Thread verwendet den gesamten dafür reservierten Stapel. Dies wird oft durch unendliche Rekursion verursacht.
Ein Thread kann den Stapel nicht erweitern, da die Seitendatei maximal ausgecheckt ist, und daher können keine zusätzlichen Seiten zum Erweitern des Stapels zugesichert werden.
Ein Thread kann den Stapel nicht erweitern, da sich das System innerhalb des kurzen Zeitraums befindet, mit dem die Seitendatei erweitert wird.
Wenn eine Funktion, die auf einem Thread ausgeführt wird, lokale Variablen zuordnet, werden die Variablen im Aufrufstapel des Threads platziert. Der für die Funktion erforderliche Stapelplatz kann so groß sein wie die Summe der Größen aller lokalen Variablen. Der Compiler führt jedoch in der Regel Optimierungen durch, die den für eine Funktion erforderlichen Stapelspeicher reduzieren. Wenn sich beispielsweise zwei Variablen in unterschiedlichen Bereichen befinden, kann der Compiler für beide Variablen den gleichen Stapelspeicher verwenden. Der Compiler kann auch einige lokale Variablen vollständig beseitigen, indem Berechnungen optimiert werden.
Der Umfang der Optimierung wird durch Compilereinstellungen beeinflusst, die zur Buildzeit angewendet werden. Beispiel: durch die /F (Set Stack Size) - C++-Compileroption.
In diesem Thema werden allgemeine Kenntnisse von Konzepten wie Threads, Threadblöcken, Stapeln und Heap vorausgesetzt. Weitere Informationen zu diesen Basiskonzepten finden Sie unter Microsoft Windows Internals von Mark Russinovich und David Solomon.
Debuggen eines Stapelüberlaufs ohne Symbole
Hier ist ein Beispiel zum Debuggen eines Stapelüberlaufs. In diesem Beispiel wird NTSD auf demselben Computer wie die Zielanwendung ausgeführt und leitet die Ausgabe an KD auf dem Hostcomputer um. Details finden Sie unter Steuern des Benutzermodusdebuggers aus dem Kerneldebugger.
Im ersten Schritt wird gezeigt, welches Ereignis dazu führte, dass der Debugger eingebrochen wurde:
0:002> .lastevent
Last event: Exception C00000FD, second chance
Sie können ausnahmecode 0xC00000FD in ntstatus.h nachschlagen. Dieser Ausnahmecode ist STATUS_STACK_OVERFLOW, der angibt , dass eine neue Schutzseite für den Stapel nicht erstellt werden kann. Alle Statuscodes werden in 2.3.1 NTSTATUS-Werten aufgeführt.
Sie können auch den Befehl "!error " verwenden, um Fehler im Windows-Debugger nachzuschlagen.
0:002> !error 0xC00000FD
Error code: (NTSTATUS) 0xc00000fd (3221225725) - A new guard page for the stack cannot be created.
Um zu überprüfen, ob der Stapel überläuft, können Sie den Befehl k (Display Stack Backtrace) verwenden:
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
Der Zielthread ist in COMCTL32!_chkstk unterteilt, was auf ein Stapelproblem hinweist. Nun sollten Sie die Stapelnutzung des Zielprozesses untersuchen. Der Prozess hat mehrere Threads, aber die wichtige ist die, die den Überlauf verursacht hat. Identifizieren Sie diesen Thread zuerst mithilfe des Befehls ~ (Threadstatus):
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
Jetzt müssen Sie Thread 2 untersuchen. Der Punkt links von dieser Zeile gibt an, dass dies der aktuelle Thread ist.
Die Stapelinformationen sind im TEB (Thread Environment Block) bei 0x7FFDC000 enthalten. Die einfachste Möglichkeit zum Auflisten ist die Verwendung von !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```
Dies erfordert jedoch, dass Sie über die richtigen Symbole verfügen. Eine schwierigere Situation ist, wenn Sie keine Symbole haben und den Befehl dd (Anzeigespeicher) verwenden müssen, um die Rohwerte an diesem Speicherort anzuzeigen:
0:002> dd 7ffdc000 L4
7ffdc000 009fdef0 00a00000 009fc000 00000000
Um dies zu interpretieren, müssen Sie die Definition der TEB-Datenstruktur nachschlagen. Verwenden Sie den Befehl "dt Anzeigetyp ", um dies auf einem System zu tun, in dem Symbole verfügbar sind.
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
...
Threaddatenstrukturen
Um mehr über Threads zu erfahren, können Sie auch Informationen zum Threadsteuerungsblock-bezogenen Strukturen ethread und kthread anzeigen. (Beachten Sie, dass hier 64-Bit-Beispiele gezeigt werden.)
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
Weitere Informationen zu Threaddatenstrukturen finden Sie unter Microsoft Windows Internals .
Wenn Sie eine 32-Bit-Version der _TEB-Struktur betrachten, zeigt sie an, dass die zweite und dritte DWORDs in der TEB-Struktur auf den unteren und oberen Rand des Stapels zeigen. In diesem Beispiel sind diese Adressen 0x00A00000 und 0x009FC000. (Der Stapel wächst im Arbeitsspeicher nach unten.) Sie können die Stapelgröße mit dem ? (Auswerten des Ausdrucks) Befehl:
0:002> ? a00000-9fc000
Evaluate expression: 16384 = 00004000
Dies zeigt, dass die Stapelgröße 16 K beträgt. Die maximale Stapelgröße wird im Feld DeallocationStack gespeichert, das Teil dieser TEB-Struktur ist. Das DeallocationStack
Feld gibt die Basis des Stapels an. Nach einer Berechnung können Sie bestimmen, dass der Offset dieses Felds 0xE0C ist.
0:002> dd 7ffdc000+e0c L1
7ffdce0c 009c0000
0:002> ? a00000-9c0000
Evaluate expression: 262144 = 00040000
Dies zeigt, dass die maximale Stapelgröße 256 K beträgt, was bedeutet, dass mehr als ausreichendEr Stapelraum übrig bleibt.
Darüber hinaus sieht dieser Prozess sauber aus – es befindet sich nicht in einer unendlichen Rekursion oder überschreitet seinen Stapelraum, indem übermäßig große stapelbasierte Datenstrukturen verwendet werden.
Unterteilen Sie sich nun in KD, und sehen Sie sich die allgemeine Systemspeicherauslastung mit dem Erweiterungsbefehl !vm an:
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)
.....
Betrachten Sie zunächst die Verwendung des nicht ausgelagerten und ausgelagerten Pools. Beide sind gut in Grenzen, daher sind diese nicht die Ursache des Problems.
Sehen Sie sich als Nächstes die Anzahl der zugesicherten Seiten an: 183528 von 202280. Dies ist sehr nahe an der Grenze. Obwohl in dieser Anzeige diese Zahl nicht vollständig im Grenzwert angezeigt wird, sollten Sie bedenken, dass während Sie das Debuggen im Benutzermodus ausführen, andere Prozesse auf dem System ausgeführt werden. Jedes Mal, wenn ein NTSD-Befehl ausgeführt wird, werden diese anderen Prozesse ebenfalls zuordnen und Arbeitsspeicher freigeben. Dies bedeutet, dass Sie nicht genau wissen, wie der Speicherzustand zum Zeitpunkt des Stapelüberlaufs war. Angesichts der Nähe der zugesicherten Seitenzahl am Grenzwert ist es sinnvoll, zu schließen, dass die Seitendatei zu einem bestimmten Zeitpunkt verwendet wurde und dies zu einem Stapelüberlauf führte.
Dies ist kein ungewöhnliches Vorkommen, und die Zielanwendung kann dafür nicht wirklich fehlerhaft sein. Wenn dies häufig der Fall ist, sollten Sie erwägen, die anfängliche Stapelzusage für die fehlerhafte Anwendung zu erhöhen.
Analysieren eines einzelnen Funktionsaufrufs
Es kann auch hilfreich sein, genau herauszufinden, wie viel Stapelraum ein bestimmter Funktionsaufruf angibt.
Dazu zerlegen Sie die ersten Anweisungen, und suchen Sie nach der Anweisungsnummersub esp
. Dadurch wird der Stapelzeiger verschoben, wobei Zahlenbytes für lokale Daten effektiv reserviert werden.
Beispiel: Verwenden Sie zuerst den Befehl "k", um den Stapel zu betrachten.
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
Verwenden Sie dann den Befehl u, ub, uu (Unassemble), um den Assemblercode an dieser Adresse anzuzeigen.
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
Dies zeigt, dass Header_Draw 0x58 Bytes von Stapelspeicher zugewiesen wurden.
Der Befehl r (Registers) enthält Informationen zum aktuellen Inhalt der Register, z. B. esp.
Debuggen des Stapelüberlaufs, wenn Symbole verfügbar sind
Symbole stellen Bezeichnungen für Elemente bereit, die im Arbeitsspeicher gespeichert sind, und wenn verfügbar, können sie das Untersuchen von Code vereinfachen. Eine Übersicht über Symbole finden Sie unter Verwenden von Symbolen. Informationen zum Festlegen des Symbolpfads finden Sie unter .sympath (Set Symbol Path).For information on setting the symbols path, see .sympath (Set Symbol Path).For information on setting the symbols path, see .sympath (Set Symbol Path).
Zum Erstellen eines Stapelüberlaufs können wir diesen Code verwenden, der weiterhin eine Unterroutine aufruft, bis der Stapel erschöpft ist.
// 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();
}
Wenn der Code kompiliert und unter WinDbg ausgeführt wird, wird er für einige Male in einer Schleife ausgeführt und löst dann eine Stapelüberlaufausnahme aus.
(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)
Verwenden Sie den Befehl "!analyse ", um zu überprüfen, ob wir tatsächlich ein Problem mit unserer Schleife haben.
...
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:
Mit dem Kb-Befehl sehen wir, dass es viele Instanzen unseres Schleifenprogramms gibt, die jeweils Arbeitsspeicher verwenden.
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]
...
Wenn Symbole verfügbar sind, können die dt-_TEB verwendet werden, um Informationen zum Threadblock anzuzeigen. Weitere Informationen zum Threadspeicher finden Sie unter Threadstapelgröße.
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
Wir können auch den !teb-Befehl verwenden, der stackBase abd StackLimit anzeigt.
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
Mit diesem Befehl können wir die Stapelgröße berechnen.
0:000> ?? int(@$teb->NtTib.StackBase) - int(@$teb->NtTib.StackLimit)
int 0n1044480
Zusammenfassung der Befehle
- k (Anzeigestapel-Backtrace)
- ~ (Threadstatus)
- d, da, db, dc, dd, dD, df, dp, dq, du, dw (Display-Speicher)
- u, ub, uu (Unassemble)
- r (Register)
- .sympath (Set-Symbolpfad)
- x (Symbole überprüfen)
- dt (Anzeigetyp)
- !analyze
- !Teb