Tutorial sobre Windbg [Parte III]

En el anterior artículo vimos cómo abrir nuestra aplicación en modo usuario dentro de Windbg y comprendimos mejor qué es lo que ocurre cuando este se hace cargo de nuestra aplicación. Ahora tenemos a nuestra disposición una ventana con salida de texto y una caja de entrada de comandos pero, ¿qué podemos hacer?

Es importante saber que podemos manejar la ventana de Windbg e introducir comandos en su caja de texto porque nuestra aplicación está detenida debido a la excepción STATUS_BREAKPOINT que se recibe por defecto nada más abrir la aplicación en Windbg. Así pues, lo primero que buscamos es una forma de permitir que nuestra aplicación continúe con su ejecución. El comando g (o presionar F5) consigue esto mismo. Este comando hace que la aplicación siga ejecutándose hasta que se encuentre con alguna condición de parada (posteriormente aprenderemos a configurar esto), o bien ocurra algún tipo de excepción (por ejemplo, la aplicación quiere acceder a una posición de memoria incorrecta).

El comando g admite alguna que otra variante. Si, por ejemplo, queremos que nuestra aplicación siga ejecutándose hasta que se alcance una determinada posición de memoria, podemos usar la sintaxis g <Dirección>. Por otra parte, si solo nos interesa que continúe la ejecución hasta que finalice la función en curso, podemos usar el comando gu.

¿Cómo detener la ejecución de la aplicación en los momentos que nos interese?

Uno de los aspectos más importantes en la depuración de aplicaciones es la creación de breakpoints. Windbg nos proporciona los medios necesarios para crear todo tipo de breakpoints, desde los más sencillos hasta los más imaginativos. Saber qué tipo de breakpoint se debe usar en cada caso nos permitirá ahorrar horas de trabajo infructuoso.

Establecer breakpoints en código

El primer tipo de breakpoints que voy a tratar son los clásicos breakpoints en código. Como su nombre indica, son breakpoints que se alcanzan en una determinada dirección de código (típicamente el inicio de una función, pero podría ser cualquier otra cosa). El comando de Windbg para crear este tipo de breakpoints es bp. Este comando recibe como parámetro la dirección en la que queremos detener la ejecución del programa y, opcionalmente, algunas restricciones y comandos que se ejecuten automáticamente cuando se llegue al breakpoint. El abanico de restricciones que se pueden establecer conforma un amplio tipo de breakpoints que se conocen como breakpoints condicionales, que se tratarán más adelante en este artículo. Por ejemplo, si introducimos el comando bp MiAplicacion!MiFuncion “kb” y pulsamos INTRO, lo que va a ocurrir es que cuando reanudemos la ejecución de nuestra aplicación (con el comando g, como ya sabe) y se llegue a la función cuyo símbolo es MiAplicacion!MiFuncion, el depurador informará sobre esto y automáticamente ejecutará el comando kb, que como verá posteriormente, sirve para ver el estado de una estructura de datos muy importante para un proceso/hilo, la pila de ejecución.

Una cosa que quizá se pregunte es, ¿cómo averiguar qué símbolo de función me interesa? Una opción de fuerza bruta consiste en mostrar en pantalla todos los símbolos que cumplan con un determinado patrón y buscar aquella función que puede interesarnos. El comando x consigue esto mismo. Por ejemplo, si quisiera mostrar los símbolos de MiAplicacion que contuvieran la palabra “Window”, podríamos usar la sintaxis x MiAplicacion!*Window*. Al pulsar INTRO, WIndbg buscará entre los símbolos disponibles aquellos que coincidan con el patrón establecido y los mostrará en pantalla junto con sus correspondientes direcciones. Si bien esta aproximación de fuerza bruta es útil en algunos casos, lo más probable es que el símbolo en el que quiera detener la ejecución del programa lo obtenga de los resultados que le proporcione Windbg al ejecutar otros comandos.

Por supuesto, también podemos crear múltiples breakpoints sin necesidad de tener que establecerlos uno a uno. Supongamos que nuestra aplicación tiene algún problema a la hora de escribir ciertos archivos en el disco. Podríamos suponer que los símbolos que más nos interesa son los de aquellas funciones que contengan la palabra “Write”. Para agregar breakpoints para todos y cada uno de los símbolos que cumplan con este patrón, podemos usar el comando bm, de la siguiente manera: bm MiAplicacion!*Write*

Si la aplicación que se está depurando tiene un cierto grado de complejidad, es bastante probable que haga uso de librerías dinámicas (DLL) cuya carga no se conoce de antemano y se produce en tiempo de ejecución. Por este motivo, si necesitara parar la ejecución en alguna función dentro de esas librerías dinámicas no le sería posible, pues Windbg no conoce la dirección de la función donde debe detenerse. A fin de cuentas, Windbg solo entiende de direcciones de memoria. Para dar solución a este problema, existe el comando bu. Por ejemplo, si nuestra aplicación hiciera uso de una DLL para calcular los primeros dígitos decimales del número pi, podemos detener la ejecución en dicha función ejecutando el comando bu MiDLL!CalcularPI. Una vez introducido el comando lo que ocurre es que Windbg sabe que justo en el momento en que el módulo correspondiente a esa DLL (MiDLL) se cargue en memoria (eche un vistazo al artículo anterior para obtener más información sobre la carga de módulos), debe crear el correspondiente breakpoint, ahora que sabe cuál es su dirección de memoria.

Establecer breakpoints por acceso a un dato

En muchos casos vamos a necesitar parar la ejecución de nuestro programa no cuando se llegue a una porción de código, sino cuando se acceda a un dato. El escenario típico de esto es una aplicación que en apariencia sufre un bug del tipo buffer overrun. Básicamente este tipo de problemas implica que se sobreescriban posiciones de memoria contiguas a las de una porción de datos del programa, por ejemplo un vector. En esta situación, quizá necesitemos indagar sobre todos y cada uno de los accesos que se produzcan hacia este vector. Podemos lograr esto ejecutando el comando ba w4 miVector, suponiendo que el símbolo miVector sea el correspondiente al vector que queremos examinar. Por supuesto, podemos sustituir el símbolo por una dirección de memoria. ¿Qué significa el w4 que aparece como primer parámetro del comando ba? “W” indica que estamos buscando accesos de tipo escritura. Otras posibilidades son “E” (ejecución) y “R” (lectura). El “4” indica el número de bytes que queremos monitorizar. En las arquitecturas de 32 bits este valor puede ser 1, 2 ó 4. En las de 64 bits existe la posibilidad de establecer también 8 bytes como tamaño.

Como habrá podido observar, la creación de breakpoints es un aspecto complejo de Windbg. La capacidad más interesante a mi juicio es la posibilidad de crear breakpoints condicionales. Como vimos, para crear un breakpoint se usa el comando bp. Uno de los parámetros de este comando va entrecomillado y representa una condición adicional a la condición estándar (la llegada a la correspondiente función). Incluso se puede omitir la función y dejar solamente la condición. Supongamos que nuestro programa tiene una variable Contador que va incrementándose cada vez que se itera en un bucle, por ejemplo. Quizá estemos interesados en parar la ejecución de nuestra aplicación cuando la variable Contador adquiera el valor 10, que sabemos que es el valor de finalización del bucle. Para ello podríamos usar esta sintaxis:

bp “.if (poi(Contador)==10) {.echo Fin de cuenta }”

Este comando establece un breakpoint (bp) cuya condición (.if) es que la variable Contador sea igual al número 10. En tal caso, sacamos por pantalla (.echo) la cadena “Fin de cuenta”. El comando poi se usa porque de no hacerlo, la variable Contador sería interpretada como una dirección de memoria, y esto no es lo que queremos. Como queremos tener acceso al contenido de esa dirección de memoria, los programadores de C que estén leyendo esto contestarán rápidamente que necesitamos desreferenciar ese acceso. En C esto se hace con el operador * y en Windbg con poi.

Cómo mostrar un listado de los breakpoints que hemos añadido

Una vez que hayamos establecido los breakpoints que necesitemos, es posible que queramos ver un listado de los mismos. Puede usar para ello el comando bl o bien usar la interfaz gráfica de Windbg: Si pulsa F9, se abrirá el cuadro de diálogo Breakpoints, desde el que podrá ver un listado de los breakpoints asociados a la sesión de depuración actual y crear de manera más o menos intuitiva otros nuevos. Mi consejo es usar siempre que sea posible la propia consola de comandos de Windbg, pues suele ofrecer más flexibilidad que la interfaz gráfica, pero comento esta posibilidad por si alguien se siente más cómodo/a usando cuadros de diálogo.

En este artículo hemos aprendido a crear breakpoints básicos y sencillos y otros no tan sencillos. Hemos visto también que la potencia de los breakpoints reside en los breakpoints condicionales, pues el único límite prácticamente es el de nuestra imaginación. La documentación de Windbg (F1) ofrece información más detallada sobre cada uno de los comandos que he tratado aquí, pero espero esto que sirva como punto de partida para sacarle el mayor provecho posible a esta funcionalidad que nos proporciona el sistema operativo en conjunción con Windbg. En los próximos artículos veremos comandos que nos permiten examinar el estado de la máquina. Para comprender mejor la salida de estos comandos, se explicarán también algunos conceptos importantes de la estructura de un computador y del sistema operativo, como son el repertorio de registros, la pila de ejecución o el bloque de control de un proceso/hilo.

2 thoughts on “Tutorial sobre Windbg [Parte III]

  1. Enhorabuena por la serie de artículos!

    Estoy comenzando la lectura del libro Windows Internals 5th edition y estos artículos tuyos me vienen muy bien para empezar a conocer ésta herramienta.

Leave a Reply

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