Debug di un overflow dello stack
Un overflow dello stack è un errore che i thread in modalità utente possono riscontrare. Questo errore può dipendere da tre cause::
Un thread usa l'intero stack riservato. Questo è spesso causato da ricorsione infinita.
Un thread non può estendere lo stack perché il file di pagina è massimo e pertanto non è possibile eseguire il commit di pagine aggiuntive per estendere lo stack.
Un thread non può estendere lo stack perché il sistema rientra nel breve periodo usato per estendere il file di pagina.
Quando una funzione in esecuzione in un thread alloca variabili locali, le variabili vengono inserite nello stack di chiamate del thread. La quantità di spazio dello stack richiesto dalla funzione può essere grande quanto la somma delle dimensioni di tutte le variabili locali. Tuttavia, il compilatore esegue in genere ottimizzazioni che riducono lo spazio dello stack richiesto da una funzione. Ad esempio, se due variabili si trovano in ambiti diversi, il compilatore può usare la stessa memoria dello stack per entrambe le variabili. Il compilatore potrebbe anche essere in grado di eliminare completamente alcune variabili locali ottimizzando i calcoli.
La quantità di ottimizzazione è influenzata dalle impostazioni del compilatore applicate in fase di compilazione. Ad esempio, dall'opzione /F (Imposta dimensioni stack) - C++.
In questo argomento si presuppone una conoscenza generale dei concetti, ad esempio thread, blocchi di thread, stack ed heap. Per altre informazioni su questi concetti di base, vedere Microsoft Windows Internals di Mark Russinovich e David Solomon.
Debug di un overflow dello stack senza simboli
Di seguito è riportato un esempio di come eseguire il debug di un overflow dello stack. In questo esempio, NTSD è in esecuzione nello stesso computer dell'applicazione di destinazione e reindirizza l'output a KD nel computer host. Per informazioni dettagliate, vedere Controllo del debugger in modalità utente dal debugger del kernel.
Il primo passaggio illustra l'evento che ha causato l'interruzione del debugger:
0:002> .lastevent
Last event: Exception C00000FD, second chance
È possibile cercare il codice di eccezione 0xC00000FD in ntstatus.h. Questo codice di eccezione è STATUS_STACK_OVERFLOW, che indica che non è possibile creare una nuova pagina di protezione per lo stack. Tutti i codici di stato sono elencati nei valori NTSTATUS 2.3.1.
È anche possibile usare il comando !error per cercare gli errori nel debugger di Windows.
0:002> !error 0xC00000FD
Error code: (NTSTATUS) 0xc00000fd (3221225725) - A new guard page for the stack cannot be created.
Per verificare l'overflow dello stack, è possibile usare il comando k (Display Stack Backtrace):
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
Il thread di destinazione è suddiviso in COMCTL32!_chkstk, che indica un problema di stack. È ora necessario esaminare l'utilizzo dello stack del processo di destinazione. Il processo ha più thread, ma quello importante è quello che ha causato l'overflow, quindi identificare questo thread prima usando il comando ~ (Stato thread):
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
È ora necessario analizzare il thread 2. Il punto a sinistra di questa riga indica che si tratta del thread corrente.
Le informazioni sullo stack sono contenute nel teb (blocco di ambiente thread) in 0x7FFDC000. Il modo più semplice per elencarlo consiste nell'usare !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```
Tuttavia, è necessario disporre dei simboli appropriati. Una situazione più difficile è quando non si dispone di simboli e si deve usare il comando dd (Display Memory) per visualizzare i valori non elaborati in tale posizione:
0:002> dd 7ffdc000 L4
7ffdc000 009fdef0 00a00000 009fc000 00000000
Per interpretare questa situazione, è necessario cercare la definizione della struttura dei dati TEB. Usare il comando dt Display Type per eseguire questa operazione in un sistema in cui sono disponibili i simboli.
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
...
Strutture dei dati dei thread
Per altre informazioni sui thread, è anche possibile visualizzare informazioni sulle strutture correlate al blocco di controllo thread ethread e kthread. Si noti che qui sono riportati esempi a 64 bit.
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
Per altre informazioni sulle strutture dei dati dei thread, vedere Microsoft Windows Internals (Interni di Microsoft Windows).
Esaminando una versione a 32 bit della struttura _TEB, indica che i secondi e i terzi DWORD nella struttura TEB puntano rispettivamente alla parte inferiore e superiore dello stack. In questo esempio, questi indirizzi sono 0x00A00000 e 0x009FC000. Lo stack aumenta verso il basso in memoria. È possibile calcolare le dimensioni dello stack usando ? (Evaluate Expression) comando:
0:002> ? a00000-9fc000
Evaluate expression: 16384 = 00004000
Ciò mostra che le dimensioni dello stack sono pari a 16 K. Le dimensioni massime dello stack vengono archiviate nel campo DeallocationStack, che fa parte di questa struttura TEB. Il DeallocationStack
campo indica la base dello stack. Dopo alcuni calcoli, è possibile determinare che l'offset del campo è 0xE0C.
0:002> dd 7ffdc000+e0c L1
7ffdce0c 009c0000
0:002> ? a00000-9c0000
Evaluate expression: 262144 = 00040000
Ciò mostra che le dimensioni massime dello stack sono pari a 256 K, il che significa che viene lasciato più spazio dello stack adeguato.
Inoltre, questo processo sembra pulito, non si trova in una ricorsione infinita o supera lo spazio dello stack usando strutture di dati basate su stack eccessivamente grandi.
A questo punto, passare a KD ed esaminare l'utilizzo complessivo della memoria di sistema con il comando !vm extension:
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)
.....
Esaminare prima di tutto l'utilizzo di pool non di paging e di paging. Entrambi sono ben entro limiti, quindi non sono la causa del problema.
Esaminare quindi il numero di pagine di cui è stato eseguito il commit: 183528 su 202280. Questo è molto vicino al limite. Anche se questo display non mostra questo numero completamente al limite, è necessario tenere presente che durante l'esecuzione del debug in modalità utente, altri processi vengono eseguiti nel sistema. Ogni volta che viene eseguito un comando NTSD, questi altri processi allocano e liberano memoria. Ciò significa che non si sa esattamente quale fosse lo stato della memoria al momento in cui si è verificato l'overflow dello stack. Dato che la chiusura del numero di pagina di cui è stato eseguito il commit è al limite, è ragionevole concludere che il file di pagina è stato usato a un certo punto e questo ha causato l'overflow dello stack.
Questo non è un evento insolito e l'applicazione di destinazione non può essere effettivamente difettosa per questo. Se si verifica spesso, è consigliabile aumentare l'impegno iniziale dello stack per l'applicazione non riuscita.
Analisi di una singola chiamata di funzione
Può anche essere utile per scoprire esattamente quanto spazio dello stack sta allocando una determinata chiamata di funzione.
A tale scopo, smontare le prime istruzioni e cercare il numero di istruzioni.sub esp
In questo modo, il puntatore dello stack riserva in modo efficace i byte numerici per i dati locali.
Ecco un esempio. Usare innanzitutto il comando k per esaminare lo stack.
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
Usare quindi il comando u, ub, uu (Unassemble) per esaminare il codice dell'assembler in tale indirizzo.
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
Ciò mostra che Header_Draw allocati 0x58 byte di spazio dello stack.
Il comando r (Registri) fornisce informazioni sul contenuto corrente dei registri, ad esempio esp.
Debug dell'overflow dello stack quando sono disponibili simboli
I simboli forniscono etichette agli elementi archiviati in memoria e, se disponibili, possono semplificare l'analisi del codice. Per una panoramica dei simboli, vedere Uso dei simboli. Per informazioni sull'impostazione del percorso dei simboli, vedere .sympath (Set Symbol Path).
Per creare un overflow dello stack, è possibile usare questo codice, che continua a chiamare una subroutine fino a quando lo stack non viene esaurito.
// 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();
}
Quando il codice viene compilato ed eseguito in WinDbg, esegue il ciclo per un certo numero di volte e quindi genera un'eccezione di overflow dello stack.
(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)
Usare il comando !analyze per verificare di avere effettivamente un problema con il ciclo.
...
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:
Usando il comando kb si noterà che sono presenti molte istanze del programma ciclo ogni uso della memoria.
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]
...
Se i simboli sono disponibili, è possibile usare il _TEB dt per visualizzare informazioni sul blocco di thread. Per altre informazioni sulla memoria del thread, vedere Thread Stack Size.For more information about thread memory, see Thread Stack Size.
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
È anche possibile usare il comando !teb che visualizza StackBase abd StackLimit.
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
È possibile calcolare le dimensioni dello stack usando questo comando.
0:000> ?? int(@$teb->NtTib.StackBase) - int(@$teb->NtTib.StackLimit)
int 0n1044480
Riepilogo dei comandi
- k (Display Stack Backtrace)
- ~ (Stato thread)
- d, da, db, dc, dd, dD, df, dp, dq, du, dw (Display Memory)
- u, ub, uu (Unassemble)
- r (registri)
- .sympath (Imposta percorso simbolo)
- x (esaminare i simboli)
- dt (tipo di visualizzazione)
- !analizzare
- !Teb