Herramientas de construcción de software
Entre el archivo de texto y el programa que corre hay cuatro etapas, y cada una tira sus propios errores. Saber cuál falló ahorra la mitad del tiempo de depuración.
01Las cuatro etapas
Escribir gcc main.c -o programa parece un solo paso, pero son cuatro programas
encadenados. Saber cuál falló es la mitad de la depuración:
| Etapa | Entra | Sale | Error típico |
|---|---|---|---|
| Preprocesador | .c | .i | No encuentra un encabezado |
| Compilador | .i | .s (ensamblador) | Error de sintaxis o de tipos |
| Ensamblador | .s | .o (objeto) | Rara vez falla |
| Enlazador | .o y bibliotecas | Ejecutable | Referencia indefinida |
El de compilación menciona archivo y línea: el compilador está leyendo tu código. El de
enlazado dice undefined reference to 'algo' y no señala ninguna línea: el código compiló
bien, pero falta el cuerpo de una función. Casi siempre es una biblioteca que no se agregó —el clásico
-lm para las funciones matemáticas— o un archivo que quedó fuera de la compilación.
Recorré las cuatro etapas y mirá en qué se va convirtiendo el archivo, con el comando que ejecuta cada una.
02Compilación separada y bibliotecas
Cada .c se compila por separado en su .o, y el enlazador los junta. La
ventaja es concreta: si se toca un solo archivo, se recompila sólo ese.
gcc -c main.c → main.o
gcc -c sensor.c → sensor.o
gcc main.o sensor.o -o programa -lm
| Tipo de biblioteca | Archivo | Cómo se usa |
|---|---|---|
| Estática | .a en Linux, .lib en Windows | Se copia dentro del ejecutable al enlazar |
| Dinámica | .so en Linux, .dll en Windows | Se carga al ejecutar; se comparte entre programas |
La estática da un ejecutable autónomo y más grande; la dinámica, uno chico que depende de que la biblioteca esté instalada. Para un equipo embebido casi siempre conviene la estática: menos sorpresas en el campo.
03make
Cuando hay más de tres archivos, recompilar a mano es inviable. make lee un archivo de reglas y rehace sólo lo que quedó desactualizado, comparando fechas de modificación:
CC = gcc
CFLAGS = -Wall -Wextra -O2
programa: main.o sensor.o
$(CC) main.o sensor.o -o programa -lm
main.o: main.c sensor.h
$(CC) $(CFLAGS) -c main.c
clean:
rm -f *.o programa
Cada regla tiene un objetivo, sus dependencias y las órdenes para construirlo,
que van indentadas con un tabulador de verdad. Si sensor.h cambia, make sabe que
main.o quedó viejo y lo rehace; si no cambió nada, no hace nada.
-Wall -Wextra encienden las advertencias, y hay que leerlas: la mayoría de los errores
en tiempo de ejecución fueron antes una advertencia ignorada. -g agrega información para
el depurador, -O2 optimiza, y -std=c11 fija la versión del lenguaje para que
el programa se comporte igual en otra máquina.
04Control de versiones
Un sistema de control de versiones guarda la historia del proyecto: qué cambió, cuándo y por qué. Con git, el ciclo básico es corto:
| Orden | Qué hace |
|---|---|
git init | Crea el repositorio |
git status | Qué cambió desde el último commit |
git add archivo | Marca lo que va a entrar en el próximo commit |
git commit -m "…" | Guarda una versión con su explicación |
git log | La historia |
git diff | Las diferencias exactas, línea por línea |
git checkout -b rama | Abre una línea de trabajo paralela |
Tres criterios que valen más que la lista de órdenes: commits chicos que hagan una sola cosa,
mensajes que expliquen el porqué y no el qué —el diff ya dice qué cambió—, y un
.gitignore que deje afuera lo generado: los .o, el ejecutable y los archivos
temporales no se versionan.
05Depurar
- printf. Rudimentario pero efectivo, sobre todo en sistemas embebidos donde no hay depurador.
- gdb. Compilando con
-g:breakpara poner un punto de parada,run,nextysteppara avanzar,printpara ver una variable ybacktracepara saber cómo se llegó hasta ahí. Con un programa que se cayó,backtracesobre el volcado de memoria suele resolver el caso en un minuto. - valgrind. Detecta accesos fuera de rango, uso de memoria sin inicializar y pérdidas de memoria. Un programa que parece andar bien puede estar lleno de errores que valgrind muestra.
- Sanitizers.
-fsanitize=addressinstrumenta el programa para que aborte en el momento exacto del acceso indebido, con la línea señalada.
Un programa que falla «a veces» casi siempre tiene memoria mal usada: un arreglo desbordado o un puntero colgante. El síntoma aparece lejos de la causa, y por eso las herramientas de análisis valen más que horas de lectura del código.
06En el laboratorio
Con un programa de diez líneas, correr gcc -E, -S y -c y
mirar los archivos intermedios. Contar cuántas líneas tiene el .i después de incluir
stdio.h.
Armar un proyecto de tres archivos con su Makefile, con objetivos all,
clean y debug. Verificar que al tocar un solo .c se recompila
sólo ese.
Generar a propósito un error de cada etapa: un #include inexistente, un punto y coma
faltante y una función declarada pero no definida. Anotar qué dice exactamente cada mensaje y
aprender a reconocerlos.
07Errores frecuentes
- Confundir error de compilación con error de enlazado.
- Olvidar
-lmal usar funciones matemáticas. - Indentar las órdenes de un Makefile con espacios en lugar de un tabulador.
- Ignorar las advertencias del compilador.
- Versionar archivos generados —objetos, ejecutables, carpetas de compilación— y ensuciar la historia.
- Depurar un binario compilado sin
-gy no ver ni nombres de variables.
08Autoevaluación
¿Qué hace exactamente el preprocesador?
Manipula texto: pega los archivos incluidos, reemplaza las macros y resuelve la compilación condicional. No entiende C.
Aparece undefined reference to 'sqrt'. ¿Qué etapa falló?
El enlazado. Falta agregar -lm: el prototipo estaba en
math.h, pero el cuerpo de la función está en la biblioteca matemática.
¿Para qué sirven los guardas #ifndef en un encabezado?
Para que el contenido no se incluya dos veces en la misma unidad de compilación, lo que provocaría definiciones duplicadas.
¿Cómo sabe make qué recompilar?
Comparando fechas de modificación: si una dependencia es más nueva que el objetivo, rehace ese objetivo.
¿Qué diferencia hay entre una biblioteca estática y una dinámica?
La estática se copia dentro del ejecutable al enlazar; la dinámica se carga al ejecutar y se comparte entre programas.
¿Qué conviene poner en un mensaje de commit?
El porqué del cambio. El qué ya lo muestra el diff.
09Para ampliar
- Brian W. Kernighan y Rob Pike. The Practice of Programming. Addison-Wesley, 1999. Depuración, pruebas y herramientas, con el criterio de dos autores que escribieron Unix.
- Scott Chacon y Ben Straub. Pro Git. 2.ª ed., Apress, 2014. Disponible en línea y en castellano. El libro de git, de lo básico al flujo de trabajo en equipo.
- John Graham-Cumming. The GNU Make Book. No Starch Press, 2015. Para cuando el Makefile crece y empieza a tener lógica propia.
- Randal E. Bryant y David R. O'Hallaron. Computer Systems: A Programmer's Perspective. 3.ª ed., Pearson, 2015. El capítulo 7 explica el enlazado con un detalle que no está en ningún otro lado.