En el post previo mencioné los mecanismos básicos para controlar la ejecución del programa: puntos de interrupción, step over y step in. En esta tercera fracción voy a completar la idea con algunos más para parar el proyecto y una explicación significativo sobre el nivel de granularidad con que se pueden indicar estos puntos de la ejecución en el código fuente. También, les voy a contar sobre algunos otras maneras especiales para regresar atrás en el tiempo o alterar (casi) arbitrariamente la ejecución, cosas que en la práctica facultan considerar algúnas veces un mismo error, o ensayar determinadas soluciones antes de codificarlas. La ventaja reside en no tener que reiniciar el proyecto y llegar una vez más hasta el punto conflictivo, lo cual puede llevar bastante tiempo o ser tedioso. Pero además, como resultado colateral, esto sentará una fundamento que permitirá realizar algunos trucos para sortear ciertas limitaciones más adelante cuando hablemos de inspección de expresiones y otras acciones similares. El último mecanismo normal para parar el proyecto según las lineas del código fuente que falta referir es el que faculta ejecutar hasta alguna línea. Este mecanismo, válido cuando el proyecto ya se ha detenido preliminarmente por cierta otra razón. Podemos ubicar el cursor en una línea de código e indicarle al depurador que continúe ejecutando usualmente hasta conseguir esa línea (Shift+F7). Antes de hacerlo, ZinjaI verifica que la línea del cursor sea válida, y si no lo es, avisa y no avanza, para eludir sorpresas. Pero si consideramos que una instrucción de código C++ fuente se puede traducir al compilar en muchas instrucciones de código de máquina, hay un nivel de granularidad más fino que el depurador podría controlar (y de hecho lo hace). Sin embargo, realizar esto solo tiene sentido si conocemos mucho de ensamblador y de como funcionan los compiladores y estamos mirando detalles muy muy finos. Por ejemplo, si estamos optimizando hasta el nivel de código de máquina (cosa que usualmente solo se hace en motores de videojuegos muy específicos, o en drivers), o si poseemos la osadía de depurar código para el cual no poseemos información de depuración o sus fuentes. Ambas situaciones no son para nada comunes entre usuarios de ZinjaI, y todavía no me han tocado a mi tampoco, así que ZinjaI todavía no tiene un conjunto de controles para ver y ejecutar instrucciones de ensamblador. Entonces es significativo a saber es que los puntos de interrupción y demás "lugares" del código fuente generalmente se le indican al depurador por número de línea, y en una línea puede haber muchas instrucciones. Por ejemplo, una línea "if (a>b) max=a; else max=b;" solo permitirá parar la ejecución justo antes de valorar la condición, y avanzar hasta después del if completo. Pero si dividimos esa linea en tres, una con el if, otra con la acción por verdadero y otra con la acción por falso, podemos ahora diferenciar las tres partes. Por esto, para depurar, conviene eludir lineas largas con muchas instrucciones, y ubicar solo una instrucción/condición por línea. controlar mejor al ejecución desde el depurador La manera que queda de parar el proyecto es recibiendo una señal. La señal la puede generar el usuario (por ejemplo, Ctrl+C en la consola), el lanzamiento de una excepción (esas cosas de try-catch-throw-etc estilo java), o el sistema operativo (como por ejemplo, en una violación de segmento). Los dos últimos casos son los más comunes, porque implican yerros que normalmente deseamos corregir y por ello estamos depurando. El primero es el que simula ZinjaI cuando usamos el botón de Pausa para parar la ejecución en un momento dado (y no por llegar a cierto punto), simula una señal de interrupción y esto hace que gdb retome el control. En verdad hay una manera más y escaso general de parar el proyecto que es generando una de estas situaciones desde el mismo proyecto que está siendo depurado. En este producto hablé de la macro _revienta, que emplea este mecanismo para hacerse pasar por un punto de interrupción, y es muy provechoso para reemplazar a assert. Ahora que conocemos ejecutar y detener, podemos pensar en cosas más raras. Una de ellas es regresar el tiempo atrás. Desde hace determinadas versiones gdb incorporó un mecanismo para deshacer la ejecución. Este mecanismo consiste en verdad en ir registrando todos los cambios de cada instrucción, guardando los valores viejos de las variables que se van modificando, y unas cuantas cosas más que llevan mucho tiempo y memoria (todo tarea del depurador). En ZinjaI figura como "Ejecución hacia atrás" en el menú de depuración. Primero hay que habilitarla para que registre todos los cambios, despues ejecutar usualmente paso a paso hacia adelante, y en determinado momento que querramos, podemos deshacer esos pasos. Esto agrada muy prometedor cuando uno lo lee, y por eso lo incorporé en ZinjaI ni bien lo implementó gdb, pero personalmente casi jamás lo he usado, y en varios casos no funciona correctamente (el depurador se cierra sin aviso, tal vez en versiones futuras de gdb se torne más estable). Otras dos alternativas más interesantes y que sí uso mucho son las de salir de una función sin terminar de ejecutarla, y la de brincar arbitrariamente por el código. La primera (return, Ctrl+F6 en ZinjaI) sirve para salir de una función sin ejecutar los que falte de la misma. Para ello, si la función no es de tipo void, debemos ingresar el valor de retorno que el depurador incluirá en la memoria como si lo hubiese retornado la función. Esto es provechoso cuando una función no retorna lo que necesitamos para continuar depurando. Pero más provechoso todavía es poder pausar la ejecución en un punto, y continuar desde otro. Supongamos que veníamos analizando una función, y empezamos a avanzar sin prestar mucha atención, y de pronto la función sale, o saltea una articula de control, o una variable coge un valor inesperado, etc. Una manera de averiguar qué ocurrió es regresar a ejecutar el programa, esta vez deteniendonos antes, o progresando paso a paso con más cuidado. Pero regresar a ejecutar el proyecto puede ser tedioso y llevar tiempo. En muchos casos basta con decirle al depurador que modifique los registros para que el proyecto crea que estaba en otra posición (antes del error) y continúe desde allí, regresando así a ejecutar la fracción que nos interesa. Un caso tradicional es cuando damos step over en una llamada a función y vemos que retorna algo inesperado. Si nos interesa ver qué pasa dentro de la función, podemos regresar a ejecutarla, pero esta vez con step in. Para eso, en ZinjaI, vamos con el cursor a la linea de la llamada y presionamos Ctrl+F5 (Continuar desde aquí). Ahora, el proyecto pensará que estaba por ejectuar esa línea, porque el depurador habrá modificado los registros que dicen donde va la ejecución. Y allí podemos continuar con step in para ver que ocurre. No siempre es posible, y hay que utilizarlo con cuidado (nos faculta por ejemplo brincar de una función a otra, pero eso no se debe, probablemente destruyamos el stack al salir de la función), pero es una de las alternativas que más uso cuando depuro. Hay que tener cuidado porque a diferencia de la ejecución hacia atrás, aquí al regresar el punto de ejecución a una instrucción previa, no volvemos los estados de las variables al estado que tenían cuando se ejecutó esa instrucción por primera vez, así que normalmente conviene regresar un escaso más a determinado punto donde se asignen las variables (por ejemplo al principio de una función) para que la segunda ejecución de la instrucción de interés sea realmente útil. En la consola se observa que el proyecto ya avanzó hasta el final mostrando el fruto (y con es_primo quedando en falso), pero en ZinjaI la saeta verde en el borde indica que se volvió el punto de ejecución (Ctrl+F5) a la línea donde inicializa la bandera. Finalmente, quedarían por tapar las alternativas del breakpoint (shift+click sobre el punto rojo), que facultan definir entre otras cosas una condición a evaluar, de manera que la ejecución continúe si la condición no se cumple. Esto es útil, pero hay que notar que el depurador internamente detendrá siempre el programa, evaluará la condición, y continuará automáticamente si no se cumple, dando la sensación de que en verdad no se detuvo. Si la linea en cuestión se ejecuta muchas veces, este procedimiento ralentizará notablemente (mucho) la ejecución del programa. Una alternativa más simple y eficiente, si se sabe de antemano cuantas veces la condición será falsa antes de ser verdadera, es introducir esa porción en el tema de "ignorar" para que ignore las primeras pasadas por esa línea. Nuevamente el depurador se detendrá, contará y seguirá, pero esto es más rápido, ya que lo lento en el caso previo es en común la evaluación de la condición.