Tutorial sobre Windbg [Parte IV]

En esta cuarta parte del tutorial sobre Windbg, se van a tratar algunos comandos básicos para conocer el estado de la máquina que estamos depurando. El primer aspecto que se va a tratar es el estado del banco de registros.

Estado del banco de registros

Toda computadora dispone de un conjunto compuesto por un cierto número de registros. El número de registros y su tamaño (en número de bits) es algo que depende de la arquitectura de la plataforma. Los registros principalmente sirven para almacenar de manera temporal pequeños valores que se van necesitando durante la ejecución del código máquina, así como valores de retorno de funciones, flags de estado del procesador, etc. Si ha seguido la parte tres del tutorial (http://msmvps.com/blogs/dmartin/archive/2009/08/13/tutorial-sobre-windbg-parte-iii.aspx), en ella se indicaron algunos comandos que sirven para detener el flujo de ejecución de un programa dentro del depurador. Si en ese estado queremos examinar cuál es el contenido de los registros de la máquina, podemos ejecutar el comando r. Este es un ejemplo de la salida del comando r en un sistema operativo de 32 bits que está siendo depurado:

0:000> r
eax=00000000 ebx=00000000 ecx=0012fb0c edx=779664f4 esi=fffffffe edi=00000000
eip=779be60e esp=0012fb28 ebp=0012fb54 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2c:
779be60e cc              int     3

En la salida del comando se muestra el contenido de los registros de la arquitectura IA-32: Eax (registro acumulador), Ebx (registro base), Ecx (registro contador), Esi (registro origen en operaciones con la memoria), Edi (registro destino en operaciones con la memoria), Eip (registro de instrucciones), Esp (registro que contiene el puntero de pila), Ebp (registro con el puntero base de pila), y algunos flags que están activados en el registro de estado del procesador.

Sin embargo, estos no son todos los registros que componen el banco de registros del procesador. Por defecto, Windbg aplica una máscara al banco de registros que sirve para que solamente se muestren los registros de enteros del procesador. El comando rm devuelve cuál es la máscara que está siendo aplicada:

0:000> rm
Register output mask is 9:
       1 - Integer state (32-bit)
       8 - Segment registers

En este ejemplo vemos que se están mostrando los registros de enteros y los registros de segmento del procesador. Si en nuestra depuración necesitáramos echar un vistazo también a los registros de punto flotante, el comando rm ? nos puede ayudar a descubrir qué máscara aplicar:

0:000> rm ?
       1 - Integer state (32-bit) or
       2 - Integer state (64-bit), 64-bit takes precedence
       4 - Floating-point state
       8 - Segment registers
      10 - MMX registers
      20 - Debug registers and, in kernel, CR4
      40 - SSE XMM registers

Como queremos establecer el bit 0 (registros de enteros), el bit 2 (registros de punto flotante) y el bit 3 (registros de segmento), formamos el número binario 1101, que en hexadecimal es el número D. Ejecutamos rm D y a continuación rm para ver la máscara aplicada:

0:000> rm D
0:000> rm
Register output mask is d:
       1 - Integer state (32-bit)
       4 - Floating-point state
       8 - Segment registers

Comprobamos que efectivamente lo hemos hecho bien:

0:000> r
eax=00000000 ebx=00000000 ecx=0012fb0c edx=777e64f4 esi=fffffffe edi=00000000
eip=7783e60e esp=0012fb28 ebp=0012fb54 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
fpcw=027F: rn 53 puozdi  fpsw=0000: top=0 cc=0000 --------  fptw=FFFF
fopcode=0000  fpip=0000:00000000  fpdp=0000:00000000
st0= 0.000000000000000000000e+0000  st1= 0.000000000000000000000e+0000
st2= 0.000000000000000000000e+0000  st3= 0.000000000000000000000e+0000
st4= 0.000000000000000000000e+0000  st5= 0.000000000000000000000e+0000
st6= 0.000000000000000000000e+0000  st7= 0.000000000000000000000e+0000

Es destacable comentar que en Windbg existe el concepto de pseudo-registros. Dependiendo de la arquitectura de la computadora en la que estemos trabajando, los nombres de los registros pueden cambiar, pese a que su semántica sea la misma. Por ejemplo, en la arquitectura x86 el registro de instrucciones es el registro Eip, mientras que en la arquitectura x64 es el registro Rip. Para evitar tener que memorizar este tipo de cosas, podemos hacer uso del pseudo-registro $ip. Aparte de este, dos de los pseudo-registros que más uso son $ra, que sirve para hacer referencia a la dirección de retorno de la función en curso, y $retreg, que hace referencia al registro que almacena el resultado de una función, una vez que esta ha finalizado (en el caso de la arquitectura x86, se trata del registro físico Eax).

Vista del código en ejecución

Otro aspecto muy importante durante una sesión de depuración es tener acceso al código ensamblador que está siendo ejecutado por el procesador. Windbg nos permite desensamblar el código máquina (binario) presente en la memoria del computador para convertirlo al lenguaje ensamblador, mucho más legible.

Para desensamblar las instrucciones de la función en curso, podemos hacer uso del comando uf . (Unassemble function). El punto que hay después de “uf” sirve como referencia a la instrucción presente en el registro de instrucciones, pero en general después de “uf” puede ponerse cualquier otra dirección de memoria.

Si queremos desensamblar instrucciones a partir de la instrucción actual (para ver qué código va a ejecutar el procesador inmediatamente después), podemos hacer uso del comando u . (o u $ip, haciendo uso de los pseudo-registros comentados anteriormente). Por defecto, si no se indica nada, Windbg desensambla 8 instrucciones.

Si en cambio quisiéramos desensamblar 8 instrucciones anteriores a la instrucción actual, el comando a ejecutar sería ub $ip Esto debería ser uno de los primeros pasos que deberíamos dar si nos hemos encontrado con un estado de la máquina incorrecto y queremos saber por qué la máquina ha llegado a tal estado, es decir, qué se ha estado ejecutando antes de que ocurriera el desastre.

Examinar el contenido de la memoria

La memoria de un computador que está ejecutando un programa contiene regiones con diferentes tipos de información. Pueden contener el código del programa, datos, la pila de ejecución, heap, etc.

La extensión de Windbg !address <Dirección> facilita el saber de qué tipo es la región que engloba a una dirección de memoria determinada. Por ejemplo:

0:000> !address $ip
 ProcessParametrs 007117a8 in range 00710000 00715000
 Environment 00710810 in range 00710000 00715000
    777a0000 : 777a1000 - 000d6000
                    Type     01000000 MEM_IMAGE
                    Protect  00000020 PAGE_EXECUTE_READ
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageImage
                    FullPath ntdll.dll

Aquí se puede ver, entre otras cosas, como la dirección de memoria que se está ejecutando actualmente contiene código (“RegionUsageImage”), lo que es algo completamente normal.

Veamos qué ocurre si ejecutamos ese mismo comando pero haciendo referencia esta vez al puntero de pila:

0:000> !address @esp
 ProcessParametrs 007117a8 in range 00710000 00715000
 Environment 00710810 in range 00710000 00715000
    00030000 : 0012e000 - 00002000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000004 PAGE_READWRITE
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageStack
                    Pid.Tid  16fc.c4c

Podemos ver que, obviamente, se trata de una región de la pila (“RegionUsageStack”).

El comando !address personalmente lo uso cuando me encuentro con una dirección de memoria involucrada en algún problema y quiero saber qué tipo de contenido tiene. Podría ser que fuera una dirección inválida, o bien que el procesador estuviera intentando ejecutar código desde una región de memoria marcada como contenedora de datos, por culpa de algún puntero que apunte a donde no debe o quién sabe si por algún fallo de hardware. Otro uso que le doy a la extensión !address es saber qué tipo de fuga de memoria tiene un proceso. Si nos encontramos con un proceso que consume memoria de manera desmedida, existe la posibilidad de que tenga una fuga de memoria. Como un proceso puede fugar diversos tipos de memoria, una manera de saber qué tipo de memoria está utilizando sin liberar es ejecutar el comando !address sin parámetros para ver un resumen estadístico del consumo de memoria de un proceso.

Para ver lo que contiene una dirección de memoria, Windbg nos proporciona la familia de comandos d*, que tienen la sintaxis

d<Tipo> <Rango_direcciones>

<Tipo> sirve para ayudar al depurador a interpretar el contenido de la memoria situado en el rango <Rango_direcciones>

En la documentación de Windbg está disponible una lista con todos los tipos de comando d* que se pueden usar, según lo que queramos mostrar sean valores ASCII, Unicode, palabras dobles, valores en punto flotante, etc. Si el resultado de un comando d* es una ristra de símbolos “?”, esto quiere decir que Windbg ha intentado acceder a una posición inválida de memoria. Esto merecería una investigación más en profundidad usando alguno de los comandos que se han descrito anteriormente.

En el próximo artículo se tratarán en detalle los comandos de Windbg que nos permiten explorar dos estructuras muy importantes en el estado de una máquina que está siendo depurada: El bloque de control de proceso (y de hilo) y la pila de ejecución.

Leave a Reply

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