¿Cómo “debuguear” un programa en Linux?

Pum! falla!

De repente las cosas fallan. Una forma en la que puede fallar un programa en Linux es mediante un "segmentation fault". Esto sucede cuando el kernel detecta que el programa intenta leer de o escribir a un área de memoria a la que no tiene ese acceso. Varias pueden ser las razones, la corrupción de los datos del programa puede ocasionar que se siga un puntero (corrupto) hacia ningún lado, o la corrupción de la pila puede hacer que se salte (volviendo de una función) a cualquier parte, etc. Cuando algo de todo eso sucede, el kernel mata al proceso por medio de la señal SIGSEGV.

El GDB

La herramienta principal para ver qué pasa en un programa es el gdb. Es un debugger de línea de comando, pero es realmente cómodo y no le envidia nada a los más gráficos.

La forma de iniciarlo es gdb ejecutable. El ejecutable será conveniente que tenga su "debug info", esto es, la información que el compilador le adosa para decirle al debugger qué líneas de código fuente corresponden a cuáles instrucciones de código máquina. El GCC agrega esta información sólo si se le pasa la opción -g en el momento de compilar.

Al comenzar, el gdb nos recibe con su "prompt" (gdb). El programa a analizar todavía no se inicia. Los comandos más interesantes son los siguientes:

run
Ejecuta el programa que se indicó al iniciar el gdb.
start
Igual que run, pero se detiene al principio de la función main.
br lugar
Instala un "breakpoint" en el lugar indicado. El lugar puede expresarse como el nombre de una función, un número de línea, o la combinación archivo-fuente:número-de-línea. Cuando el programa se detiene ahí, el GDB queda "parado" en ese lugar, y el comando "l" muestra el contexto.

Pusimos un breakpoint. Corrimos el programa y se detuvo. Ahora nos interesa examinar el estado de las cosas, y quizá ir viendo como es estado cambia conforme hacemos avanzar el programa de a pasitos...

l
Lista código fuente. Si no le pasamos nada muestra la ubicación en la que acaba de detenerse el programa. Si volvemos a poner "l" avanza y muestra lo que sigue (y con enters, sigue avanzando, como explico más abajo). También se puede indicar un lugar a listar, de la misma manera en que se indicaba un breakpoint.
p expresión
Imprime el valor de la expresión.
n
Ejecuta la siguiene instrucción, sin entrar en funciones llamadas.
s
Igual, pero entra en las funciones.

La pila de llamadas es la estructura en la que se organizan las llamadas recursivas que se fueron haciendo y que nos dejaron en el punto en el que estamos. Por ejemplo main() llama a inicializar() que a su vez llama a abrir_archivos(). A cada nivel de la pila se lo llama "frame". Los comnandos relacionados con la pila son los siguientes:

bt
La muestra. Muestra cómo llegamos a donde estamos, qué funciones fueron llamadas. Los numeritos de cada frame sirven para ir directo ahí con el comando "fr".
up
Va hacia arriba, es decir, nos paramos en el punto en el que fue llamado el procedimiento en el que estábamos parados. De hacer muchos "up" seguidos terminamos en main. Con el comando "l" inspeccionamos el nuevo "frame".
down
... y volvemos con down.
fr
Muestra el frame, el nivel, en el que estamos parados. También permite cambiar el frame según los números que aparecen en el comando bt.

Algunas cosas importantes a saber:

Debug post-mortem

Una característica interesante que tiene Linux es que cuando el proceso muere de esa manera, puede crear un archivo que congele el estado de todas las variables, y de todo lo que pasa en ese proceso. Luego, con un "debugger" uno puede entrar y analizar el programa casi como si todavía estuviera corriendo.

Para activar esa funcionalidad hay que usar el comando ulimit de esta manera: ulimit -c unlimited. Se puede activar eso para todo el sistema editando el archivo /etc/security/limits.conf, y añadiendo una línea tipo "* soft core unlimited" (por lo menos en Debian y Ubuntu).

Con esa función activada, se escribirá un archivo de nombre "core" en el directorio actual del proceso. Para entrar al gdb y ver qué pasó, se deberá ejecutar gdb ejecutable archivo-core. Y listo!

Valgrind

Valgrind es una herramienta que analiza la ejecución de un programa mediante un truco muy astuto: emula al procesador, reemplaza a la PC por un entorno simulado, sin que el programa se de cuenta. De esa manera permite detectar toda una serie de cuestiones de manera mucho más precisa. También permite analizar en dónde un pograma está "perdiendo memoria" (memory leaks). Para instalarlo hay que instalar el paquete de ese nombre de la distribución, en Debian/Ubuntu eso es hacer: apt-get install valgrind.


Mis otros artículos sobre programación en Linux.


Por Nicolás Lichtmaier. Cualquier comentario o pedido de mayor claridad o extensión será bien recibido.