Partilhar via


Depurando 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 página 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 página.

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 algumas variáveis locais inteiramente otimizando cálculos.

A quantidade de otimização é influenciada pelas configurações do compilador aplicadas no tempo de build. 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 thread, 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 Controling the User-Mode Debugger from the Kernel Debugger for details.

A primeira etapa é ver qual evento fez com que o depurador interrompa:

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

Você pode pesquisar o código de exceção 0xC00000FD em ntstatus.h. Esse 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 status são listados em Valores 2.3.1 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 marcar que a pilha estoura, 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 é aquele que causou o estouro, portanto, identifique esse thread primeiro usando o comando ~ (Status do 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 

Agora você precisa investigar o thread 2. O período à esquerda dessa linha indica que esse é o thread atual.

As informações de pilha estão contidas no TEB (Bloco de Ambiente de Thread) 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 exige 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, você precisa pesquisar a definição da estrutura de dados TEB. Use o comando dt Display Type para fazer isso em um sistema em que 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.

Observando uma versão de 32 bits da estrutura _TEB, 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 é de 16 K. O tamanho máximo da pilha é armazenado no campo DeallocationStack. Após algum cálculo, você pode determinar que o deslocamento desse campo é 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 mais do que o espaço de pilha adequado é deixado.

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

Agora, interrompa o KD e examine o uso geral da memória do sistema com o 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)
         ..... 

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

Em seguida, examine o número de páginas confirmadas: 183528 de 202280. Isso é muito próximo do limite. Embora essa exibição não mostre que esse número está completamente no limite, você deve ter em mente que, enquanto estiver executando a depuração no modo de usuário, outros processos estão em execução 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 o estouro da pilha ocorreu. Considerando o quão próximo o número da página confirmada está no limite, é razoável concluir que o arquivo de página foi usado em algum momento e isso causou o estouro da pilha.

Isso não é uma ocorrência incomum, e o aplicativo de destino não pode realmente ser culpado por isso. Se isso acontecer com frequência, talvez você queira considerar o aumento do 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 de instruçãosub esp. Isso move o ponteiro de 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 (Unassemble) 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 bytes 0x58 alocados de espaço em pilha.

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

Estouro de pilha de depuração 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 de 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 esse código, que continua a chamar uma sub-rotina até que a pilha esteja esgotada.

// 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 em WinDbg, ele faz loop por alguns números de vezes e, em seguida, gera 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 marcar que 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 há muitas instâncias do nosso programa de loop 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 os símbolos estiverem disponíveis, o dt _TEB poderá ser usado para exibir informações sobre o bloco de thread. Para obter mais informações sobre a memória do 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 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

Podemos calcular o tamanho da pilha usando esse comando.

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

Resumo dos comandos

Confira também

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

/F (Definir Tamanho da Pilha) – Opção do compilador C++