lunes, 1 de septiembre de 2014

Entendiendo la tabla de inspecciones (parte 2)

Siguiendo con esta serie de artículos sobre cómo hace ZinjaI para mostrar las inspecciones durante la depuración, ahora voy contar un poco más qué pasa por debajo, en gdb, el verdadero depurador. Si alguien alguna vez usó directamente gdb sabrá que su interfaz es básicamente una linea de comandos, donde uno ingresa por ejemplo "break hola.cpp:12" para agregar un punto de interrupción, o "print x" para ver qué hay en la variable "x". Sin embargo, cuando uno programa un front-end para gdb (una interfaz, como ZinjaI), puede utilizar un conjunto diferente de comandos que otorgan una salida más uniforme y predecible para que analizarla automáticamente sea más simple y eficiente. Esta interfaz alternativa (también tipo linea de comandos) se conoce como MI (creo que por machine-interface).

Con la interfaz usual de gdb tengo que hacer "print x" cada vez que quiero ver cuanto vale "x", y antes tal vez ejecutar algún otro comando para ubicarme en el frame que corresponda (lo que en ZinjaI aparece como "nivel"). Con la interfaz MI , puedo en algún momento crear lo que se llama variable-object (VO) para simplificar la tarea. Esto es una especie de objeto que me ofrece gdb y que representa una inspección en particular asociada a un frame en particular. Además de mantener por sí mismo la relación con su frame, el VO tiene otra ventaja: no necesito preguntar cuanto vale en cada paso para ver si cambió, sino que puedo pedirle a gdb que me liste solo los VO que cambiaron desde la última vez que pregunté. Así, en cada paso, en lugar de yo tener que enviarle a gdb varios comandos para reevaluar todas las inspecciones una por una y determinar qué hay de nuevo, solo le tengo que preguntar cuales cambiaron (un solo comando) y él me muestra justo lo que necesito (recordando además el scope de cada variable). Este mecanismo, así planteado es el ideal, y es de lo más eficiente ya que reduce al mínimo la comunicación con gdb (una de las cosas que más penaliza la velocidad de respuesta de la interfaz) y le delega a gdb el trabajo sucio (saber cuando cambian o cuando dejan de ser válidas).

Así quiero que se vean las estructuras en la tabla de inspecciones.

Pero los VOs presentan algunos "problemas" para la interfaz que proponía en el artículo anterior. Tienen una estructura jerárquica, de modo que un VO de un objeto compuesto (por ej, un struct), contiene dentro varios VOs hijos, uno por cada atributo del mismo (de forma similar ocurre en un arreglo con cada elemento). El problema es que para ver su contenido tengo que consultar por los VOs hijos uno por uno, no puedo hacerlo con el que los contiene. En consecuencia, el comando de gdb que consulta los VOs modificados, sólo lista los VOs hijos que cambian al cambiar algo dentro del objeto, pero nunca lista el VO que representa al objeto completo. Y mantener un VO por cada hijo puede ser caro y trabajoso, porque la cuestión es recursiva. Es decir, para saber mediante VOs si cambia algo en un "struct S { int a,b; } x;" tendría que generar VOs para "x.a" y "x.b" por separado, y luego tomarme el trabajo de concatenar los resultados para obtener algo como "{a=7,b=5}", que es lo que quiero mostrar en la tabla de inspecciones. Tampoco es una locura hacerlo, pero el detalle importante es que esto que quiero es lo que se obtiene sin esfuerzo con la interfaz de usuario normal (con "print x"). Pueden ver en este enlace un ejemplo de sesión de depuración con uno y otro mecanismo.

Piensen el caso de una arreglo "int v[1000]" donde necesitaría 1000 VOs, para notar que no es una estrategia ideal generar VOs por cada elemento/atributo. Entonces, lo que hace ZinjaI es tratar a estas inspecciones de forma especial, y evaluarlas con la interfaz normal (con "print"). Esto era literalmente así hasta hace poquito, sin depender de un VO. Por cada una de estas inspecciones, tenía que pedirle a gdb que se posicione en el frame adecuado, para lo cual tenía que hacer un seguimiento de los frames, y utilizar un par de comandos extra, rezando para que el número de frame/nivel sea suficiente como para identificarlos. Esta clase de cosas hacen que cuando se carga mucho la tabla de inspecciones el paso a paso se torne mucho más lento. Ahora, implementé una pequeña mejora: creo un VO auxiliar para obtener la dirección de la variable en el scope que corresponde, y luego uso el comando "print" con esa dirección.

Diagrama de flujo simplificado de lo que pasa internamente al agregar
una inspección a la tabla (hecho con el editor de PSeInt :P )

Por ejemplo, si quiero inspeccionar "x" (siendo x el struct de arriba), al crear el VO para "x" veo que esta tiene VOs hijos. Esto me indica que es una expresión compuesta, cuyo valor no voy a poder ver directamente con ese VO, (y especialmente que tampoco voy a poder detectar sus modificaciones). Entonces, inmediatamente creo el VO auxiliar "&x" (que me dará la dirección del objeto, y estará asociado al mismo scope). Supongamos que la dirección (el resultado de evaluar este VO auxiliar) es "0xffffd8e3" Luego, para ver de forma "completa" el contenido del mismo objeto "x", y además sin importar el ámbito desde el cual lo hago, utilizo el comando "print *((S*)0xffffd8e3)". Es decir, mediante la interfaz regular, pero consultando con la dirección exacta que me provee el VO auxiliar. Entonces, algo como "string s" se convierte en "*((string*)&s)", mientras que algo como "int m[10][8]" se convierte en "*((int(*)[8])&m)@10" (el @10 significa para "p" algo como: mostrame este y los 9 que siguen), donde &s y &m son los resultados que me dan los VOs auxiliares.

Con esta idea, sigo teniendo que reevaluar las inspecciones de objetos y arreglos en cada paso de tiempo para saber si cambiaron en algo, pero ya no tengo problemas con los distintos ámbitos, y reduzco así al mínimo los pasos de comunicación necesarios para la consulta. En consecuencia, espero que esto mejore sensiblemente la velocidad de respuesta de la ejecución paso a paso para las próximas versiones de ZinjaI. De momento, al menos me simplifica la implementación que sustenta a la tabla de inspecciones, y soluciona el problema de los ámbitos ambigüos. La próxima vez que escriba les cuento qué pasa con las que no tienen un ámbito asociado.

No hay comentarios:

Publicar un comentario