Partilhar via


Depuração de um Stack Overflow

Um estouro de pilha é um erro que os threads do modo de usuário podem encontrar. Existem três possíveis causas para esse erro:

  • Um thread usa toda a pilha reservada para ele. Isso geralmente é causado por recursão infinita.

  • Um thread não pode estender a pilha porque o arquivo de paginação está no máximo e, portanto, nenhuma página adicional pode ser confirmada para estender a pilha.

  • Um thread não pode estender a pilha porque o sistema está dentro do breve período usado para estender o arquivo de paginação.

Quando uma função em execução em um thread aloca variáveis locais, as variáveis são colocadas na pilha de chamadas do thread. A quantidade de espaço de pilha exigida pela função pode ser tão grande quanto a soma dos tamanhos de todas as variáveis locais. No entanto, o compilador geralmente executa otimizações que reduzem o espaço de pilha exigido por uma função. Por exemplo, se duas variáveis estiverem em escopos diferentes, o compilador poderá usar a mesma memória de pilha para ambas as variáveis. O compilador também pode ser capaz de eliminar totalmente algumas variáveis locais otimizando os cálculos.

A quantidade de otimização é influenciada pelas configurações do compilador aplicadas no momento da compilação. Por exemplo, pela opção /F (Definir Tamanho da Pilha) – Compilador C++.

Este tópico pressupõe conhecimento geral de conceitos, como threads, blocos de threads, pilha e heap. Para obter informações adicionais sobre esses conceitos básicos, consulte Microsoft Windows Internals de Mark Russinovich e David Solomon.

Depurando um estouro de pilha sem símbolos

Aqui está um exemplo de como depurar um estouro de pilha. Neste exemplo, o NTSD está em execução no mesmo computador que o aplicativo de destino e está redirecionando sua saída para KD no computador host. Consulte Controlar o depurador de modo de usuário do depurador de kernel para obter mais informações.

A primeira etapa é ver qual evento causou a interrupção do depurador:

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

Você pode pesquisar 0xC00000FD de código de exceção em ntstatus.h, Este código de exceção é STATUS_STACK_OVERFLOW, o que indica que uma nova página de proteção para a pilha não pode ser criada. Todos os códigos de status estão listados em 2.3.1 Valores NTSTATUS.

Você também pode usar o comando !error para pesquisar erros no Depurador do Windows.

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

Para verificar novamente se a pilha estourou, você pode usar o 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 

O thread de destino foi dividido em COMCTL32!_chkstk, o que indica um problema de pilha. Agora você deve investigar o uso da pilha do processo de destino. O processo tem vários threads, mas o importante é o que causou o estouro, então identifique esse thread primeiro usando o comando ~ (Thread Status):

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 

Agora você precisa investigar o thread 2. O ponto à esquerda desta linha indica que este é o thread atual.

As informações da pilha estão contidas no TEB (Thread Environment Block) em 0x7FFDC000. A maneira mais fácil de listá-lo é usando !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```

No entanto, isso requer que você tenha os símbolos adequados. Uma situação mais difícil é quando você não tem símbolos e precisa usar o comando dd (Memória de exibição) para exibir os valores brutos nesse local:

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

Para interpretar isso, é necessário consultar a definição da estrutura de dados TEB. Use o comando dt Tipo de exibição para fazer isso em um sistema onde os símbolos estão disponíveis.

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

Estruturas de dados de thread

Para saber mais sobre threads, você também pode exibir informações sobre as estruturas relacionadas ao bloco de controle de thread ethread e kthread. (Observe que exemplos de 64 bits são mostrados aqui.)

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

Consulte Microsoft Windows Internals para obter mais informações sobre estruturas de dados de thread.

Olhando para uma versão de 32 bits da estrutura _TEB, ela indica que o segundo e o terceiro DWORDs na estrutura TEB apontam para a parte inferior e superior da pilha, respectivamente. Neste exemplo, esses endereços são 0x00A00000 e 0x009FC000. (A pilha cresce para baixo na memória.) Você pode calcular o tamanho da pilha usando o ? (Avaliar expressão) comando:

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

Isso mostra que o tamanho da pilha é 16 K. O tamanho máximo da pilha é armazenado no campo DeallocationStack, que faz parte dessa estrutura TEB. O DeallocationStack campo indica a base da pilha. Após alguns cálculos, você pode determinar que o deslocamento desse campo está 0xE0C.

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

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

Isso mostra que o tamanho máximo da pilha é de 256 K, o que significa que resta mais do que o espaço adequado da pilha.

Além disso, esse processo parece limpo - não está em uma recursão infinita ou excedendo seu espaço de pilha usando estruturas de dados baseadas em pilha excessivamente grandes.

Agora entre no KD e examine o uso geral da memória do sistema com o comando de extensão !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)
         ..... 

Primeiro, examine o uso do pool não paginado e paginado. Ambos estão bem dentro dos limites, então não são a causa do problema.

Em seguida, observe o número de páginas confirmadas: 183528 de 202280. Isso está muito próximo do limite. Embora essa exibição não mostre que esse número está completamente no limite, lembre-se de que, enquanto estiver executando a depuração no modo de usuário, outros processos estão sendo executados no sistema. Cada vez que um comando NTSD é executado, esses outros processos também estão alocando e liberando memória. Isso significa que você não sabe exatamente como era o estado da memória no momento em que ocorreu o estouro da pilha. Dado o quão próximo o número da página confirmada está do limite, é razoável concluir que o arquivo de paginação foi usado em algum momento e isso causou o estouro da pilha.

Essa não é uma ocorrência incomum, e o aplicativo de destino não pode realmente ser criticado por isso. Se isso acontecer com frequência, talvez você queira considerar aumentar o compromisso de pilha inicial para o aplicativo com falha.

Analisando uma única chamada de função

Também pode ser útil descobrir exatamente quanto espaço de pilha uma determinada chamada de função está alocando.

Para fazer isso, desmonte as primeiras instruções e procure o número da instruçãosub esp. Isso move o ponteiro da pilha, reservando efetivamente bytes numéricos para dados locais.

Veja um exemplo. Primeiro, use o comando k para examinar a pilha.

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

Em seguida, use o comando u, ub, uu (Desmontar) para examinar o código do assembler nesse endereço.

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 

Isso mostra que Header_Draw alocado 0x58 bytes de espaço na pilha.

O comando r (Registros) fornece informações sobre o conteúdo atual dos registros, como esp.

Depuração de estouro de pilha quando os símbolos estão disponíveis

Os símbolos fornecem rótulos para itens armazenados na memória e, quando disponíveis, podem facilitar o exame do código. Para obter uma visão geral dos símbolos, consulte Usando símbolos. Para obter informações sobre como definir o caminho dos símbolos, consulte .sympath (Definir caminho do símbolo).

Para criar um estouro de pilha, podemos usar este código, que continua a chamar uma sub-rotina até que a pilha se esgote.

// 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 o código é compilado e executado no WinDbg, ele faz um loop por um certo número de vezes e, em seguida, lança uma exceção de estouro de pilha.

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

Use o comando !analyze para verificar se realmente temos um problema com nosso loop.

...

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 o comando kb, vemos que existem muitas instâncias do nosso programa de loop, cada uma usando memória.

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 houver símbolos disponíveis, o _TEB dt pode ser usado para exibir informações sobre o bloco de rosca. Para obter mais informações sobre a memória de thread, consulte Tamanho da pilha 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

Também podemos usar o comando !teb que exibe o StackBase e o 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

Podemos calcular o tamanho da pilha, usando este comando.

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

Resumo dos comandos

Confira também

Introdução ao WinDbg (modo de usuário)

/F (Definir Tamanho da Pilha) - Opção do Compilador C++