Debuggen eines Stapelüberlaufs

Ein Stapelüberlauf ist ein Fehler, auf den Benutzermodusthreads auftreten können. Für diesen Fehler gibt es drei mögliche Ursachen:

  • Ein Thread verwendet den gesamten dafür reservierten Stapel. Dies wird häufig durch unendliche Rekursion verursacht.

  • Ein Thread kann den Stapel nicht erweitern, da die Auslagerungsdatei maximal überschritten ist und daher keine zusätzlichen Seiten zum Erweitern des Stapels commitsiert werden können.

  • Ein Thread kann den Stapel nicht erweitern, da sich das System innerhalb des kurzen Zeitraums befindet, der zum Erweitern der Auslagerungsdatei verwendet wird.

Wenn eine Funktion, die auf einem Thread ausgeführt wird, lokale Variablen zuordnet, werden die Variablen auf dem Aufrufstapel des Threads platziert. Die menge des von der Funktion benötigten Stapelplatzes 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 benötigten Stapelplatz reduzieren. Wenn sich beispielsweise zwei Variablen in unterschiedlichen Bereichen befinden, kann der Compiler denselben Stapelspeicher für beide Variablen verwenden. Der Compiler kann auch einige lokale Variablen vollständig eliminieren, indem er Berechnungen optimiert.

Der Umfang der Optimierung wird durch Compilereinstellungen beeinflusst, die zur Buildzeit angewendet werden. Beispielsweise durch die /F (Set Stack Size) – C++-Compileroption.

Dieses Thema setzt allgemeine Kenntnisse über Konzepte wie Threads, Threadblöcke, Stapel und Heap voraus. Weitere Informationen zu diesen Basiskonzepten finden Sie unter Microsoft Windows Internals von Mark Russinovich und David Solomon.

Debuggen eines Stapelüberlaufs ohne Symbole

Hier sehen Sie ein Beispiel für das 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. Weitere Informationen finden Sie unter Steuern des User-Mode Debuggers über den Kerneldebugger .

Der erste Schritt besteht darin, zu sehen, welches Ereignis den Debugger verursacht hat:

0:002> .lastevent 
Last event: Exception C00000FD, second chance 

Sie können Ausnahmecode 0xC00000FD in ntstatus.h suchen. Dieser Ausnahmecode ist STATUS_STACK_OVERFLOW, was angibt, dass eine neue Schutzseite für den Stapel nicht erstellt werden kann. Alle status-Codes sind in 2.3.1 NTSTATUS-Werten aufgeführt.

Sie können auch den Befehl !error verwenden, um Fehler im Windows-Debugger zu suchen.

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 überlaufen wurde, 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 verfügt über mehrere Threads, aber der wichtige ist der, der den Überlauf verursacht hat. Identifizieren Sie diesen Thread zuerst mit dem Befehl ~ (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 

Nun müssen Sie Thread 2 untersuchen. Der Punkt links von dieser Zeile gibt an, dass es sich um den aktuellen Thread handelt.

Die Stapelinformationen sind im TEB (Threadumgebungsblock) unter 0x7FFDC000 enthalten. Die einfachste Möglichkeit, sie aufzulisten, 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 (Speicher anzeigen) verwenden müssen, um die Rohwerte an dieser Stelle 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 zu den strukturen ethread und kthread im Zusammenhang mit dem Threadsteuerelementblock 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 .

Bei einer 32-Bit-Version der _TEB-Struktur zeigt dies, dass die zweite und die dritte DWORDs in der TEB-Struktur auf den unteren bzw. oberen Rand des Stapels zeigen. In diesem Beispiel werden diese Adressen 0x00A00000 und 0x009FC000. (Der Stapel wächst im Arbeitsspeicher nach unten.) Sie können die Stapelgröße mit dem berechnen . (Ausdruck auswerten) 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. Nach einiger Berechnung können Sie feststellen, 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 Stapelspeicher übrig bleibt.

Darüber hinaus sieht dieser Prozess sauber aus – er befindet sich nicht in einer unendlichen Rekursion oder überschreitet seinen Stapelraum, indem er übermäßig große stapelbasierte Datenstrukturen verwendet.

Brechen Sie nun in KD ein, und sehen Sie sich die gesamte Systemspeicherauslastung mit dem !vm-Erweiterungsbefehl 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)
         ..... 

Sehen Sie sich zunächst die Verwendung von nicht ausgelagerten und ausgelagerten Pools an. Beide liegen innerhalb von Grenzen, sodass diese nicht die Ursache des Problems sind.

Sehen Sie sich als Nächstes die Anzahl der verpflichteten Seiten an: 183528 von 202280. Dies ist sehr nahe an der Grenze. Obwohl diese Anzeige nicht anzeigt, dass diese Zahl vollständig am Grenzwert liegt, 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, weisen auch diese anderen Prozesse Arbeitsspeicher zu und geben sie frei. Das bedeutet, dass Sie nicht genau wissen, wie der Speicherzustand zum Zeitpunkt des Stapelüberlaufs war. Angesichts der Nähe der gebundenen Seitenzahl an der Grenze ist es sinnvoll, zu schließen, dass die Seitendatei irgendwann verbraucht wurde und dies den Stapelüberlauf verursacht hat.

Dies ist kein ungewöhnlicher Fall, und die Zielanwendung kann dafür nicht wirklich beanstandet werden. Wenn dies häufig geschieht, sollten Sie die anfängliche Stapelzusage für die fehlerhafte Anwendung erhöhen.

Analysieren eines einzelnen Funktionsaufrufs

Es kann auch nützlich sein, genau herauszufinden, wie viel Stapelraum ein bestimmter Funktionsaufruf zuzuweisen hat.

Zerlegen Sie dazu die ersten Anweisungen, und suchen Sie nach der Anweisungsnummersub esp. Dadurch wird der Stapelzeiger verschoben und die Anzahlbytes für lokale Daten reserviert.

Es folgt ein Beispiel. Verwenden Sie zunächst 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 stapelspeichern.

Der Befehl r (Registers) stellt Informationen zum aktuellen Inhalt der Register bereit, z. B. esp.

Debuggen eines Stapelüberlaufs, wenn Symbole verfügbar sind

Symbole stellen Bezeichnungen für elemente bereit, die im Arbeitsspeicher gespeichert sind, und können, sofern verfügbar, das Untersuchen von Code erleichtern. Eine Übersicht über Symbole finden Sie unter Verwenden von Symbolen. Informationen zum Festlegen des Symbolpfads finden Sie unter .sympath (Set Symbol Path).

Um einen Stapelüberlauf zu erstellen, 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 einige Male schleifen und dann eine Stapelüberlaufausnahme auslösen.

(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 !analyze , um zu überprüfen, ob tatsächlich ein Problem mit unserer Schleife vorliegt.

...

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 Befehl kb 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, kann 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 Befehl !teb 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

Wir können die Stapelgröße mit diesem Befehl berechnen.

0:000> ?? int(@$teb->NtTib.StackBase) - int(@$teb->NtTib.StackLimit)
int 0n1044480

Zusammenfassung der Befehle

Weitere Informationen

Erste Schritte mit WinDbg (Benutzermodus)

/F (Stapelgröße festlegen) – C++-Compileroption