La parte de identificar la causa puede ser muy tediosa cuando llegar al punto en que se produce o manifiesta el error lleva bastante tiempo de ejecución. Porque el procesamiento previo es costoso, porque hay que cargar muchos datos, porque depende de acciones manuales del usuario, porque no sabemos bien cuándo va a manifestarse, por lo que sea. Y a veces hay que repetirlo y repetirlo para ir acotando el problema, porque cuando vemos el error, nos damos cuenta de que nos falta información, de que tenemos que modificar un poco el código para que genere y/o muestre más resultados intermedios y volver a probar. Hoy les comento un truco que, utilizando señales, permite generar en cualquier momento, desde cualquier punto de la ejecución, un volcado de datos ad-hoc y arbitrario hacia un archivo, para analizar el estado del programa.
Vamos entonces con los contenidos previos: señales. Las señales son un mecanismo muy básico de comunicación entre procesos que provee un sistema operativo. Se le envía una señal a un hilo o un proceso, y este se interrumpe. Si el hilo/proceso había registrado un "manejador" para la señal, se lo invoca. Sino, el sistema provee un comportamiento por defecto. La señal más común en GNU/Linux es SIGTERM, la que se envía con el comando "kill" para pedirle a un proceso que finalice ya (por las buenas, en caso de negarse se lo "obliga" con la señal SIGKILL). Otras señales se generan por combinaciones de teclas especiales. En una terminal, Ctrl+C envía SIGINT para detener un proceso. También tenemos en la terminal Ctrl+Z, que envía SIGTSTP, para suspender el proceso y enviarlo a segundo plano. Otra señal que vemos más de lo que quisieramos es SIGSEGV, la que envía el kernel cuando se da cuenta que un proceso quiere acceder a un lugar de la memoria al que no debe (cuando metemos la pata usando punteros).
Ejemplo: un código C++ que registra un manejador propio para SIGTSTP.
En cualquier momento, con Ctrl+Z veo el valor de x.
Pero hay más. Este mecanismo me viene como anillo al dedo para mejorar mis estrategias de depuración. Me pasaba que cuando depuraba un error del mallador, llegaba a cierta parte en donde veía que algo no andaba bien, y entonces necesitaba guardar el resultado intermedio. Lo normal era cortar la depuración, agregar una linea que invoque al método que guarda la malla, y luego volver a correr el programa en el depurador. Esto, como se imaginarán, lleva su tiempo y se torna tedioso, además de llenar el código de basura que después puedo olvidarme de sacar. Pero si tengo un manejador para una señal que guarde lo que yo quiero, le puedo pedir al depurador que le envíe la señal al proceso en cualquier momento. Cuando hago esto con gdb, gdb simula los datos de la señal y continúa con la ejecución del programa, de forma que inmediatamente la reciba y pase a esta función especial. Como luego de la misma seguirá ejecutando normalmente, solo tengo que tener cuidado de poner un punto de interrupción al final, o en la linea en la que estaba pausada la ejecución antes de la señal, y eso es todo.
Así entonces puedo ejecutar una función con código arbitrario desde cualquier lugar/momento de la depuración, sin tener que reiniciarla ni agregar basura en el código que realmente quiero depurar. Si ese código depende de unas pocas variables globales, puedo controlar fácilmente su comportamiento (elegir por ejemplo qué instancia de malla guardar si hay más de una, simplemente cambiandole el valor a una variable desde la tabla de inspecciones). Hago enfasis en el "arbitrario" de "código arbitrario", porque gdb permite ejecutar ciertos métodos o funciones como parte de la evaluación de una inspección, pero tiene limitaciones. Por ejemplo, para poder llamar a un método éste no debe alocan ni liberar memoria dinámicamente. Entonces se restringe mucho su uso (igual es útil para utilizar por ejemplo una sobrecarga de un operador [] para ver el contenido de una clase Vector sin meternos en sus entrañas, pero no es este el caso).
Este truco, además de servir para esas situaciones donde cuesta tiempo o paciencia llegar al punto en que se manifiesta el error, también sirve para volcar datos con el formato que más nos convenga cuando las estructuras son complejas y por ello difíciles o tediosas de explorar solo con el panel de inspecciones. Además, mientras la señal no se usa, esto no genera ninguna sobrecarga en la ejecución. Asi, siempre estará a mano para la depuración y no tendrá absolutamente ningún efecto colateral en la ejecución normal.
Los pasos 1 y 2 (código) se realizan solo una vez, luego se utiliza ese código
desde el depurador cada vez que se quiera con los pasos 4 y 5.
Por todo esto, agregué en el menú Depuración de ZinjaI un par de ítems para pedirle a gdb que simule señales y para configurar cómo debe comportarse ante una señal generada desde afuera (si debe pausar la depuración al recibirla, o si debe dejarla llegar al proceso depurado o filtrarla antes). El botón para enviar la señal se puede colocar en la barra de herramientas de depuración. Entonces ahora todo esto que explicaba se resume a una linea de código en el main para decir cuál es la función especial (programada completamente a gusto y configurable desde el panel de inspecciones) y un botón en la barra de herramientas que durante cualquier pausa de la depuración la ejecuta.
Como todo, hay un pero, y es que en Windows todavía no logro que esto funcione bien. La variedad de señales en Windows es muchísimo menor que la de los sistemas POSIX (practicamente todos los demás), algunas tienen canales especiales (particularmente SIGINT, que ya me ha dado dolores de cabeza en otros casos), y no he logrado que gdb actúe correctamente cambiando la configuración de las mismas. Pero no somos pocos los que trabajamos día a día sobre el sistema del pingüino, así que espero que el truco les resulte útil.
Espero algun dia saber tanto como vos . Un alumno
ResponderEliminar