Compartir a través de


Depuración de un desbordamiento de pila

Un desbordamiento de pila es un error que pueden encontrar los subprocesos en modo de usuario. Hay tres causas posibles para este error.

  • Un subproceso usa toda la pila reservada para él. Esto suele deberse a la recursividad infinita.

  • Un subproceso no puede extender la pila porque el archivo de página está máximo y, por tanto, no se puede confirmar ninguna página adicional para extender la pila.

  • Un subproceso no puede extender la pila porque el sistema está dentro del breve período usado para extender el archivo de página.

Cuando una función que se ejecuta en un subproceso asigna variables locales, las variables se colocan en la pila de llamadas del subproceso. La cantidad de espacio de pila requerido por la función podría ser tan grande como la suma de los tamaños de todas las variables locales. Sin embargo, el compilador normalmente realiza optimizaciones que reducen el espacio de pila requerido por una función. Por ejemplo, si dos variables están en ámbitos diferentes, el compilador puede usar la misma memoria de pila para ambas variables. El compilador también puede eliminar algunas variables locales por completo mediante la optimización de cálculos.

La cantidad de optimización se ve afectada por la configuración del compilador aplicada en tiempo de compilación. Por ejemplo, por la opción del compilador /F (Establecer tamaño de pila): C++.

En este tema se presupone conocimientos generales de conceptos, como subprocesos, bloques de subprocesos, pila y montón. Para obtener información adicional sobre estos conceptos básicos, consulte Microsoft Windows Internals by Mark Russinovich y David Solomon.

Depuración de un desbordamiento de pila sin símbolos

Este es un ejemplo de cómo depurar un desbordamiento de pila. En este ejemplo, NTSD se ejecuta en el mismo equipo que la aplicación de destino y redirige su salida a KD en el equipo host. Consulte Control del depurador en modo de usuario desde el depurador de kernel para obtener más información.

El primer paso es ver qué evento provocó que el depurador se interrumpa:

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

Puede buscar código de excepción 0xC00000FD en ntstatus.h, Este código de excepción es STATUS_STACK_OVERFLOW, que indica que no se puede crear una página de protección nueva para la pila. Todos los códigos de estado se enumeran en valores NTSTATUS 2.3.1.

También puede usar el comando !error para buscar errores en el depurador de Windows.

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

Para comprobar que la pila se ha desbordado, puede usar el 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 

El subproceso de destino se ha dividido en COMCTL32!_chkstk, lo que indica un problema de pila. Ahora debe investigar el uso de la pila del proceso de destino. El proceso tiene varios subprocesos, pero el importante es el que provocó el desbordamiento, por lo que identifique primero este subproceso mediante el comando ~ (Estado del subproceso):

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 

Ahora debe investigar el subproceso 2. El punto situado a la izquierda de esta línea indica que se trata del subproceso actual.

La información de la pila se encuentra en el bloque de entorno de subprocesos (TEB) en 0x7FFDC000. La manera más fácil de enumerar es usar !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```

Sin embargo, esto requiere que tenga los símbolos adecuados. Una situación más difícil es cuando no tiene símbolos y necesita usar el comando dd (Mostrar memoria) para mostrar los valores sin procesar en esa ubicación:

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

Para interpretar esto, debe buscar la definición de la estructura de datos TEB. Use el comando dt Display Type para hacerlo en un sistema en el que haya símbolos 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
...

Estructuras de datos de subprocesos

Para obtener más información sobre los subprocesos, también puede mostrar información sobre el bloque de control de subprocesos estructuras relacionadas con ethread y kthread. (Tenga en cuenta que aquí se muestran ejemplos de 64 bits).

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 obtener más información sobre las estructuras de datos de subprocesos.

Al examinar una versión de 32 bits de la estructura de _TEB, indica que los DWORD segundo y tercer de la estructura TEB apuntan a la parte inferior y superior de la pila, respectivamente. En este ejemplo, estas direcciones se 0x00A00000 y 0x009FC000. (La pila crece hacia abajo en la memoria). Puede calcular el tamaño de la pila mediante ? (Evaluar expresión) mandar:

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

Esto muestra que el tamaño de la pila es de 16 K. El tamaño máximo de la pila se almacena en el campo DeallocationStack, que forma parte de esta estructura TEB. El DeallocationStack campo indica la base de la pila. Después de un cálculo, puede determinar que el desplazamiento de este campo es 0xE0C.

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

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

Esto muestra que el tamaño máximo de la pila es 256 K, lo que significa que se deja más espacio de pila adecuado.

Además, este proceso parece limpio, no está en una recursividad infinita ni supera su espacio de pila mediante estructuras de datos excesivamente grandes basadas en pila.

Ahora divida en KD y examine el uso general de memoria del sistema con el 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)
         ..... 

En primer lugar, examine el uso del grupo paginado y no paginado. Ambos están bien dentro de los límites, por lo que no son la causa del problema.

A continuación, examine el número de páginas confirmadas: 183528 de 202280. Esto está muy cerca del límite. Aunque esta pantalla no muestra que este número esté completamente en el límite, debe tener en cuenta que mientras está realizando la depuración en modo de usuario, otros procesos se ejecutan en el sistema. Cada vez que se ejecuta un comando NTSD, estos otros procesos también asignan y liberan memoria. Esto significa que no sabe exactamente cuál era el estado de memoria en el momento en que se produjo el desbordamiento de la pila. Dado que el número de página confirmada está cerca del límite, es razonable concluir que el archivo de página se usó en algún momento y esto provocó el desbordamiento de la pila.

Esto no es una repetición poco frecuente y la aplicación de destino no puede ser realmente errónea para esto. Si ocurre con frecuencia, es posible que desee considerar la posibilidad de elevar el compromiso de pila inicial para la aplicación con errores.

Análisis de una sola llamada de función

También puede ser útil averiguar exactamente cuánto espacio de pila asigna una llamada de función determinada.

Para ello, desensamble las primeras instrucciones y busque el número de instrucciónsub esp. Esto mueve el puntero de pila, reservando eficazmente los bytes numéricos para los datos locales.

A continuación se muestra un ejemplo: En primer lugar, use el comando k para examinar la pila.

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

A continuación, use el comando u, ub, uu (Unassemble) para examinar el código del ensamblador en esa dirección.

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 

Esto muestra que Header_Draw asignados 0x58 bytes de espacio de pila.

El comando r (Registers) proporciona información sobre el contenido actual de los registros, como esp.

Depuración de desbordamiento de pila cuando hay símbolos disponibles

Los símbolos proporcionan etiquetas a los elementos almacenados en la memoria y, cuando están disponibles, pueden facilitar el examen del código. Para obtener información general sobre los símbolos, consulte Uso de símbolos. Para obtener información sobre cómo establecer la ruta de acceso de símbolos, vea .sympath (Establecer ruta de acceso de símbolos).

Para crear un desbordamiento de pila, podemos usar este código, que continúa llamando a una subrutina hasta que se agote la pila.

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

Cuando el código se compila y se ejecuta en WinDbg, se repite un número de veces y, a continuación, se produce una excepción de desbordamiento de pila.

(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 el comando !analyze para comprobar que realmente tenemos un problema con nuestro bucle.

...

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: 

Con el comando kb vemos que hay muchas instancias de nuestro programa de bucles con 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] 

...

Si los símbolos están disponibles, se puede usar el _TEB dt para mostrar información sobre el bloque de subprocesos. Para obtener más información sobre la memoria de subprocesos, consulte Tamaño de pila de subprocesos.

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

También podemos usar el comando !teb que muestra 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 el tamaño de la pila mediante este comando.

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

Resumen de comandos

Consulte también

Introducción a WinDbg (modo de usuario)

/F (Establecer tamaño de pila): opción del compilador de C++