P1N6Ü1N0 - C - Herramientas |
|||
![]() Inicio C Perl Caml Shell GTK SQL |
Aquí hablaremos de las últimas herramientas surgidas para facilitar la programación y el mantenimiento de aplicaciones en C. Para os hagais una idea, no se hablará de make, está obsoleto. Sigue siendo necesario, pero ahora ya no vale la pena aprender a utilizarlo, existen otras herramientas de más alto nivel que generan Makefile's por nosotros. Veremos también cómo manejar un depurador. Existen muchos programadores, incluso algunos experimentados, de C que nunca utilizaron un depurador. En muchos casos porque les daba pereza aprender. Aquí veremos como manejar los más comunes. En el futuro se añadirán más herramientas a esta sección, por ahora aquí teneis algunas comentadas:
Seguramente estaréis ya acostumbrados a compilar programas por el ya clásico método de "./configure;make;make install". Pues aunque existen casos en que el programador de la aplicación creó el configure y el Makefile, generalmente esto se hace mediante unas herramientas llamadas automake y autoconf. Seguramente muchos os estaréis preguntando para que sirve todo eso. Por qué razón se crean los scripts configure. El nombre dice bastante, habla de configurar algo. No configura el programa, sino la compilación del mismo. Este script busca las características del sistema donde se está compilando el programa que pueden afectar de un modo u otro a la compilación del mismo. Además crea por nosotros los Makefile (si lo deseamos) que sean necesarios. Esta configuración se almacena en un archivo de cabecera, que generalmente se llama config.h, y sirve para que la aplicación, mediante macros de C, sepa cómo es el sistema donde se está ejecutando. Los scripts configure son extremadamente grandes. Es fácil adivinar que no se generan a mano (aunque no sería la primera vez que alguien lo hace). Estos scripts se crean con la herramienta autoconf. Los pasos para conseguir que nuestra aplicación tenga un script configure que cree este config.h y los Makefile, son los siguientes:
Una vez realizado todo este proceso, deberíais tener un archivo Makefile.in, un ./configure, y un monton de archivos que no sabréis para que son (ni yo, sinceramente) pero que aparecen en cualquier código fuente de otros programas y son necesarios para el script configure y make. Una ventaja bastante importante de aplicar estas herramientas a nuestro código fuente es que es fácilmente "debianizable", porque es perfectamente comprensible para las herramientas de la distribución Debian dedicadas a crear paquetes a partir de código fuente. Y quien crea un .deb, crea un .rpm de forma inmediata con alien...
Son las herramientas fundamentales para depurar nuestros programas. La primera tiene un objetivo más genérico que la segunda, pues nos permite depurar todo nuestro código, mientras que la segunda simplemente depura las llamadas al sistema. Sin embargo las dos son muy útiles, la primera por su potencia y la segunda por su especifidad. GDB Tiene básicamente dos modos de funcionamiento, uno en el que cargamos el programa y luego vamos depurándolo, y otro en el que cogemos un archivo core que contiene la información del programa y su memoria en el momento de producir un fallo (una violación de segmento generalmente). El segundo modo de funcionamiento puede ser muy útil, pero en las distribuciones modernas la generacion de archivos core está desactivada por defecto. Por tanto voy a centrarme mas bien en el primero de ellos. Yo no domino todos los comandos de GDB, sólo los que para mi fueron necesarios hasta el momento, asi que esos serán los que comente, si necesitais utilizarlo más a fondo, teneis documentación en formato info. Voy a empezar explicando como depurar un pequeño programa que nos termina de forma anormal con un fallo de segmentación. Para ello, el primer paso es la ejecución limpia, sin poner ningún punto de parada (luego explicaré más esto al ver los breakpoints) Lo primero que tenemos que hacer es asegurarnos de que el programa se genera con la opción de añadir información para depurado (-g). Y además, hay que asegurarse de que no estamos optimizando en absoluto el código, para lo cual hay que utilizar la opción -O0. La segunda cosa es importante, porque si optimizamos el código, puede que no se ejecute como esperamos ya que determinadas funciones se compilan como inline, lo que significa que no se produce una llamada a función, sino que cuando en el código las llamamos, en el ejecutable se inserta el código de la función directamente. Por tanto, si no queremos ver fenómenos paranormales cuando estemos depurando el código, lo mejor es no optimizarlo (para depurar). Una vez que estos pasos fueron seguidos, llega el momento de cargar el programa y ejecutarlo desde el depurador. Para hacer esto empezaremos ejecutando el comando: gdb nombre_de_programa Esto hace que aparezca un intérprete de comandos diferente a la shell. Para ejecutar de forma totalmente limpia, como habíamos dicho antes, deberemos poner el comando: run Esto ejecutará el programa y cuando este falle, nos dirá la línea exacta de nuestro código fuente que produjo el fallo de segmentación. Parece bastante sencillo, verdad? Y entonces estareis preguntándoos, por qué razón el gdb tiene tantas opciones, y por qué os hablé antes de los breakpoints si basta con hacer un simple run. La respuesta es simple, los errores de los programas frecuentemente no se encuentran en el punto donde se produce el fallo de segmentación. Muchas veces el error viene de antes. Es muy típico, por ejemplo que en un bucle en el que recorremos un array, nos salgamos en una posición, por lo que escribimos en memoria que no está reservada, y que al hacer el gdb, nos diga que el programa falla al hacer el siguiente malloc. Por problemas cómo este, el depurador es mucho más complejo que aquel simple run, que si bien en muchos casos nos soluciona la vida, no siempre es válido. Además, existe otro problema, y son las funciones que se llaman desde muchos sitios del código. Si hacemos unas rutinas para manejar listas enlazadas, es muy probable que una función de añadir elementos se llame desde muchos puntos distintos del código fuente. Si el gdb nos dice que el fallo de segmentación se produjo en ella, y sabemos que está bien, necesitaríamos saber desde que punto del programa fue llamada para saber de donde puede venir el fallo. Para esto último tenemos el comando: backtrace Este programa, en cualquier momento, nos recorre la pila inversamente para saber desde que función fue llamada la siguiente. Por ejemplo, si nos falló en la función de añadir un elemento a la lista enlazada, posiblemente el backtrace nos diga que la función anterior era main, o también es posible que fuera otra función distinta, que a su vez fue llamada desde main. Visto esto, vamos a ver otros comandos interesantes, y los conceptos de breakpoint y ejecución paso a paso. Un breakpoint es un punto en el que se dentendrá la ejecución del programa si nosotros lo queremos así. Esta característica la maneja el procesador de nuestro ordenador, y Linux da soporte para utilizarla. Por ahora no es sencillo explicar la utilidad que tienen, asi que veamos primero la ejecución paso a paso y luego volvemos a este punto. La ejecución paso a paso es otra de las características que nos ofrecen los procesadores modernos, permiten ejecutar un programa instrucción a instrucción. En gdb tenemos dos formas de hacer esto, una es el comando: nextque ejecuta el programa entrando en las subrutinas, y la otra es el comando: stepque ejecuta las funciones tambien, pero no entra dentro, es decir, que hasta que las funciones no terminan, no nos permite hacer nada más. Sólo con esto no lograríamos nada, simplemente que el programa se ejecutase más despacio, pero es que además gdb permite ver en cualquier momento el estado de las variables del programa, y esto ya es mucho más interesante, porque podemos ver el efecto que tiene sobre las variables la ejecución de cada una de las instrucciones de nuestro programa. Ahora bien, supongamos que creemos que un gran trozo de nuestro programa es completamente correcto, o que tenemos al principio un bucle muy grande, del orden del millon de iteraciones, que sabemos que esta perfectamente, seria demasiado tedioso ir paso a paso hasta que saliéramos de esa zona. Para esto podemos utilizar los breakpoints. un breakpoint se pone con el comando: break <posicion>donde posición puede ser un número de línea o un nombre de función. Por ejemplo, para empezar a ejecutar un programa paso a paso, podemos utilizar break main No estoy seguro si esto se puede hacer de otra forma más elegante, supongo que sí, pero esta es una forma tan válida como cualquier otra. Para ver el contenido de las variables, podemos utilizar el comando: print nombre_de_variable STRACE Este programa es el más sencillo de utilizar, tiene unas cuantas opciones, pero básicamente nos bastará con teclear: strace nombre_de_programapara obtener resultados. Esto nos sacará por pantalla todas las llamadas a sistema que ejecutó nuestro programa, con los parámetros que lo hizo y las respuestas que dió el sistema, indicando en caso de devolver -1 (error), la razón. Generalmente sacará mucha más información de la que esperamos, aunque el programa sea pequeño, asi que es recomendable redireccionar su salida a un archivo. |
||
| Los gráficos de esta página han sido creados con GIMP. |