Tutorial sobre Windbg [Parte VI]

En la quinta parte de este tutorial introductorio de depuración de aplicaciones vimos algunos comandos de Windbg que nos permiten mostrar información sobre la pila de ejecución en un determinado momento, una tarea bastante cotidiana en el ámbito de la depuración. En esta parte veremos algunas otras tareas relacionadas con la pila de ejecución y los correspondientes comandos de Windbg que nos permite llevarlas a cabo.

Puede darse el caso de que necesitemos saber el espacio que ocupa cada marco de pila de una traza de pila concreta. Para ello podemos obtenerlo de dos formas: Aplicando la teoría que vimos en la parte anterior, el tamaño de cada marco de pila se podría obtener restando la dirección del puntero base (registro ebp) del marco de pila anterior a la del marco de pila actual. Recuerde que las direcciones de los punteros base aparecen bajo la columna "ChildEBP” en la salida de la familia de comandos k* de Windbg. Alternativamente, se puede hacer uso del comando kf, que hace todo este cálculo por nosotros. La salida de este comando nos mostrará el espacio ocupado por cada marco de pila, exceptuando el actual. Esta es una salida de ejemplo del comando kf de Windbg:

0:000> kf
  Memory  ChildEBP RetAddr 
          0020f868 77c0e351 ntdll!LdrpDoDebuggerBreak+0x2c
      15c 0020f9c4 77bf9048 ntdll!LdrpInitializeProcess+0x125c
       50 0020fa14 77beb365 ntdll!_LdrpInitialize+0x78
       10 0020fa24 00000000 ntdll!LdrInitializeThunk+0x10

A modo de ejemplo, vamos a calcular el tamaño del segundo marco de pila. Tenemos que restar las direcciones 0020f9c4 y 0020f868, que coincide con el contenido actual del registro ebp. Windbg incorpora un comando muy práctico que nos permite evaluar este tipo de expresiones, ?:

0:000> ? 0020f9c4-0020f868
Evaluate expression: 348 = 0000015c

Como vemos, el resultado hexadecimal obtenido es 15c, que coincide con lo que nos muestra el comando kf.

Otro comando relacionado que nos permite evaluar y convertir expresiones a diferentes formatos es .formats. Veamos un ejemplo de la salida de este comando:

0:000> .formats 0020f9c4-0020f868
Evaluate expression:
  Hex:     0000015c
  Decimal: 348
  Octal:   00000000534
  Binary:  00000000 00000000 00000001 01011100
  Chars:   ...\
  Time:    Thu Jan 01 01:05:48 1970
  Float:   low 4.87652e-043 high 0
  Double:  1.71935e-321

A veces hay que ayudar a Windbg para que muestre una buena traza de pila

En determinados ambientes de servidores es bastante común que tengamos que depurar un sistema que haya recibido una carga de trabajo bastante elevada. Una consecuencia de esto es que la información correspondiente a la pila no reside en memoria principal. Técnicamente, se dice que las páginas correspondientes a la pila han paginado a disco. Otro escenario común es que estemos depurando un software cuya ejecución haya corrompido la pila. Para que Windbg pueda construir una buena traza de pila, es necesario que ciertas estructuras de la misma estén en estado intacto. De no ser así, con total seguridad no podemos confiar en la traza de pila resultante que obtengamos mediante la familia de comandos k*. Un ejemplo de traza de pila sospechosa sería aquella en la que el marco de pila actual (es decir, el que está el primero en la traza) se le haya hecho corresponder con una dirección de memoria hexadecimal en lugar de el símbolo de una función. ¿Por qué motivo Windbg no ha podido hacer corresponder esa dirección de memoria con un módulo? Posiblemente porque con anterioridad se haya sobreescrito partes importantes de la pila (como la dirección de retorno) y el flujo de ejecución haya acabado ahí por error. Es más que probable que la dirección ni siquiera sea código ejecutable, con la correspondiente excepción que surgirá cuando el procesador se disponga a ejecutar a partir de ella.

En estos casos de la vida real, no nos queda otra opción que ayudar a Windbg aplicando nuestros conocimientos para reconstruir una buena traza de pila. Lo primero es hacer uso del comando dd (display memory, double-word) para mostrar el contenido de la pila (almacenado en la dirección de memoria contenida en el registro puntero de pila, esp):

dd esp

De la salida de este comando hay que identificar aquellas direcciones que se correspondan con funciones. Para ver los rangos de direcciones de los símbolos de todos los módulos cargados en memoria, podemos hacer uso del comando

x *!

Buscamos en la salida del comando dd esp aquellas direcciones que podrían corresponderse con direcciones de funciones. Para ello comparamos cada dirección con los rangos de direcciones que nos muestra el comando x *!. Una vez que hayamos identificado alguna dirección de memoria que sea potencialmente candidata de ckrrresponderse con una función, podemos comprobar si estamos en lo cierto haciendo uso del comando

ln <Dirección>

El comando ln (list nearest symbols) nos muestra los símbolos más cercanos a la dirección de memoria pasada como argumento.

Una vez identificadas las posibles funciones que conformarían nuestra traza de pila reconstruida, debemos tener en cuenta las direcciones de retorno que se almacenan en la pila previamente a una llamada a subrutina (como vimos en el artículo anterior). Desensamblando unas cuantas instrucciones alrededor de esa dirección de retorno mediante el comando u podemos confirmar nuestra teoría viendo si hay una llamada a subrutina (call, en ensamblador) y en cuyo caso, a qué dirección de función está llamando. Si nuestras averiguaciones han sido correctas, dicha llamada debería ser a la función que esté justo encima en nuestra hipotética traza de pila. Una vez identificado el flujo de ejecución de la aplicación, podemos dar visto bueno a nuestra traza de pila y proseguir con la depuración.

En el próximo artículo vamos a ver un ejemplo práctico donde reconstruyo una traza de pila para poner en práctica y afianzar totalmente los pasos anteriormente comentados. Seguiremos tratando el tema de la pila de ejecución con otros casos del mundo real con los que me he encontrado en los que ha sido necesario comprobar que una traza de pila es correcta para seguidamente analizarla y determinar por qué ha ocurrido cierto problema.

4 thoughts on “Tutorial sobre Windbg [Parte VI]

  1. Excelente, Dani. Solo tengo una pregunta. ¿No sería preferible usar la orden ‘dds esp’, ahorrando el esfuerzo de buscar manualmente la correspondencia con posibles símbolos o módulos cargados?

  2. @Ramón Sola: Sí, se puede usar “dds esp” y se obtendrían directamente los símbolos a partir de la información de la pila. Si hay corrupción, esos símbolos no serán correctos, pero el comando dds nos mostrará también las direcciones de retorno, para que comencemos a investigar.

    Otro comando equivalente a “dds esp” es “kd”.

  3. Muy buenos estos tutoriales de Windbg. En Windows 7 x64 estaba limitado a Windbg (a falta de mi amado Ollydbg que sólo sirve para x86) y más de una vez usé tus ayudas… hasta que descubrí IDA Pro.

    Una duda que tengo desde hace años: ¿Un “dump” de un proceso podría volver a “integrarse” al proceso? Digamos que se hace un dump de un reproductor de video, se cierra y se lanza otra instancia y se le carga ese dump para restaurar la misma instancia anterior. Volví a pensar en este problema con una de las features que ví de Mac OSX Lion (lo de suspender y recuperar estados de aplicaciones)

    Saludos

  4. @Erwin Ried: IDA Pro es un excelente “desensamblador”. Es otra de las herramientas que uso a menudo.

    Sobre lo que comentas de recuperar el estado de procesos, es algo que he visto en grupos de investigación de mi universidad, en sistemas Unix/Linux, por supuesto. Según lo que pude ver, implementarlo es complicado, puesto que por ejemplo no queda claro qué hacer con los descriptores de fichero que pudiera tener abiertos el proceso. La tabla de descriptores de ficheros no forma parte del espacio de direcciones del proceso, así que no aparece en el volcado.

    Creo recordar que para recuperar los descriptores de fichero abiertos lo que se hacía en la implementación que te comento es agregar un manejador de la señal SIGQUIT al proceso y en ella volcar la información sobre los descriptores de fichero abiertos y sus desplazamientos a un fichero de texto para recuperarlos después.

    Es posible que este campo de investigación ya esté más avanzado a día de hoy y Apple ya esté capacitado para implementar algo como esto para la próxima versión de Mac OS X. Será un paso importante para la industria de la computación, sin duda.

Leave a Reply

Your email address will not be published. Required fields are marked *