Partager via


Débogage d’un débordement de pile

Un dépassement de capacité de pile est une erreur que les threads en mode utilisateur peuvent rencontrer. Les trois causes possibles de cette erreur sont les suivantes :

  • Un thread utilise la pile entière réservée pour celle-ci. Cela est souvent dû à une récursivité infinie.

  • Un thread ne peut pas étendre la pile, car le fichier de page est maximal et, par conséquent, aucune page supplémentaire ne peut être validée pour étendre la pile.

  • Un thread ne peut pas étendre la pile, car le système est au cours de la courte période utilisée pour étendre le fichier de page.

Lorsqu’une fonction s’exécutant sur un thread alloue des variables locales, les variables sont placées sur la pile des appels du thread. La quantité d’espace de pile requise par la fonction peut être aussi grande que la somme des tailles de toutes les variables locales. Toutefois, le compilateur effectue généralement des optimisations qui réduisent l’espace de pile requis par une fonction. Par exemple, si deux variables se trouvent dans des étendues différentes, le compilateur peut utiliser la même mémoire de pile pour ces deux variables. Le compilateur peut également être en mesure d’éliminer entièrement certaines variables locales en optimisant les calculs.

La quantité d’optimisation est influencée par les paramètres du compilateur appliqués au moment de la génération. Par exemple, par l’option /F (Définir la taille de la pile) - Option du compilateur C++.

Cette rubrique suppose une connaissance générale des concepts, tels que les threads, les blocs de threads, la pile et le tas. Pour plus d’informations sur ces concepts de base, reportez-vous à Microsoft Windows Internals by Mark Russinovich et David Salomon.

Débogage d’un dépassement de capacité de pile sans symboles

Voici un exemple de débogage d’un dépassement de capacité de pile. Dans cet exemple, NTSD s’exécute sur le même ordinateur que l’application cible et redirige sa sortie vers KD sur l’ordinateur hôte. Veuillez consulter la section Contrôler le débogueur en mode utilisateur depuis le débogueur noyau pour plus de détails.

La première étape consiste à voir quel événement a provoqué l’arrêt du débogueur :

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

Vous pouvez rechercher le code d’exception 0xC00000FD dans ntstatus.h, ce code d’exception est STATUS_STACK_OVERFLOW, ce qui indique qu’une nouvelle page de garde pour la pile ne peut pas être créée. Tous les codes d’état sont répertoriés dans les valeurs 2.3.1 NTSTATUS.

Vous pouvez également utiliser la commande !error pour rechercher des erreurs dans le débogueur Windows.

0:002> !error 0xC00000FD
Error code: (NTSTATUS) 0xc00000fd (3221225725) - A new guard page for the stack cannot be created.

Pour vérifier que la pile a dépassé, vous pouvez utiliser la commande 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 

Le thread cible a été divisé en COMCTL32 !_chkstk, ce qui indique un problème de pile. Vous devez maintenant examiner l’utilisation de la pile du processus cible. Le processus comporte plusieurs threads, mais l’important est celui qui a provoqué le dépassement de capacité. Identifiez donc d’abord ce thread à l’aide de la commande ~ (État du 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 

Vous devez maintenant examiner le thread 2. La période à gauche de cette ligne indique qu’il s’agit du thread actuel.

Les informations de pile sont contenues dans le TEB (bloc d’environnement de thread) à 0x7FFDC000. Le moyen le plus simple de la lister est d’utiliser !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```

Toutefois, cela vous oblige à avoir les symboles appropriés. Une situation plus difficile est lorsque vous n’avez aucun symbole et que vous devez utiliser la commande dd (Mémoire d’affichage) pour afficher les valeurs brutes à cet emplacement :

0:002> dd 7ffdc000 L4 
7ffdc000   009fdef0 00a00000 009fc000 00000000 

Pour interpréter cela, vous devez rechercher la définition de la structure de données TEB. Utilisez la commande dt Display Type pour effectuer cette opération sur un système où les symboles sont disponibles.

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
...

Structures de données de thread

Pour en savoir plus sur les threads, vous pouvez également afficher des informations sur les structures associées aux blocs de contrôle de thread ethread et kthread. (Notez que des exemples 64 bits sont présentés ici.)

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

Pour plus d’informations sur les structures de données de thread, consultez Microsoft Windows Internals.

En examinant une version 32 bits de la structure _TEB, il indique que les deuxième et troisième DWORD de la structure TEB pointent vers le bas et le haut de la pile, respectivement. Dans cet exemple, ces adresses sont 0x00A00000 et 0x009FC000. (La pile augmente vers le bas en mémoire.) Vous pouvez calculer la taille de la pile à l’aide du ? (Évaluer l’expression) commander:

0:002> ? a00000-9fc000
Evaluate expression: 16384 = 00004000 

Cela montre que la taille de la pile est de 16 K. La taille maximale de la pile est stockée dans le champ DeallocationStack, qui fait partie de cette structure TEB. Le DeallocationStack champ indique la base de la pile. Après un calcul, vous pouvez déterminer que le décalage de ce champ est 0xE0C.

0:002> dd 7ffdc000+e0c L1 
7ffdce0c   009c0000 

0:002> ? a00000-9c0000 
Evaluate expression: 262144 = 00040000 

Cela montre que la taille maximale de la pile est de 256 K, ce qui signifie que plus d’espace de pile adéquat est laissé.

De plus, ce processus semble propre : il n’est pas dans une récursivité infinie ou dépasse son espace de pile à l’aide de structures de données basées sur une pile excessivement volumineuses.

Maintenant, divisez en KD et examinez l’utilisation globale de la mémoire système avec la commande d’extension !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)
         ..... 

Tout d’abord, examinez l’utilisation des pools non paginés et paginés. Les deux sont bien dans les limites, donc ce ne sont pas la cause du problème.

Ensuite, examinez le nombre de pages validées : 183528 sur 202280. C’est très proche de la limite. Bien que cet affichage n’affiche pas complètement ce nombre à la limite, gardez à l’esprit que pendant que vous effectuez le débogage en mode utilisateur, d’autres processus s’exécutent sur le système. Chaque fois qu’une commande NTSD est exécutée, ces autres processus allouent et libèrent de la mémoire. Cela signifie que vous ne savez pas exactement quel est l’état de mémoire au moment où le dépassement de capacité de la pile s’est produit. Étant donné que le numéro de page validé est proche de la limite, il est raisonnable de conclure que le fichier de page a été utilisé à un moment donné et que cela a provoqué le dépassement de capacité de la pile.

Il ne s’agit pas d’une occurrence rare, et l’application cible ne peut pas vraiment être défaillante pour cela. Si cela se produit fréquemment, vous pouvez envisager de déclencher l’engagement initial de la pile pour l’application défaillante.

Analyse d’un appel de fonction unique

Il peut également être utile de déterminer exactement la quantité d’espace de pile qu’un certain appel de fonction alloue.

Pour ce faire, désassemblez les premières instructions et recherchez le numéro d’instructionsub esp. Cela déplace le pointeur de pile, réservant efficacement des octets de nombre pour les données locales.

Voici un exemple. Utilisez d’abord la commande k pour examiner la pile.

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

Utilisez ensuite la commande u, ub, uu (Unassemble) pour examiner le code assembleur à cette adresse.

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 

Cela montre que Header_Draw alloué 0x58 octets d’espace de pile.

La commande r (Registers) fournit des informations sur le contenu actuel des registres, tels que esp.

Dépassement de capacité de la pile de débogage lorsque des symboles sont disponibles

Les symboles fournissent des étiquettes aux éléments stockés en mémoire et, lorsqu’ils sont disponibles, peuvent faciliter l’examen du code. Pour obtenir une vue d’ensemble des symboles, consultez Utilisation de symboles. Pour plus d’informations sur la définition du chemin des symboles, consultez .sympath (Définir le chemin des symboles).

Pour créer un dépassement de capacité de pile, nous pouvons utiliser ce code, qui continue d’appeler une sous-routine jusqu’à ce que la pile soit épuisée.

// 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();
}

Lorsque le code est compilé et exécuté sous WinDbg, il effectue une boucle pendant un certain nombre de fois, puis lève une exception de dépassement de capacité de pile.

(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)

Utilisez la commande !analyze pour vérifier que nous avons effectivement un problème avec notre boucle.

...

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: 

À l’aide de la commande kb, nous voyons qu’il existe de nombreuses instances de notre programme de boucle chacune utilisant la mémoire.

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] 

...

Si des symboles sont disponibles, la _TEB dt peut être utilisée pour afficher des informations sur le bloc de thread. Pour plus d’informations sur la mémoire du thread, consultez La taille de la pile de threads.

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

Nous pouvons également utiliser la commande !teb qui affiche 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

Nous pouvons calculer la taille de la pile à l’aide de cette commande.

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

Résumé des commandes

Voir aussi

Prise en main de WinDbg (mode utilisateur)

/F (Définir la taille de la pile) - Option du compilateur C++