wandres.dev
APRENDIZ · Setup y toolchain

Las banderas que importan

El dialecto, los avisos, la optimización y la depuración: qué hace realmente cada bandera, por qué -O2 cambia lo que el compilador es capaz de ver, y cuál es el conjunto mínimo que un profesional no negocia.

⏱ 15 min

La línea de compilación no es burocracia: es la configuración de tu detector de errores más potente. Cada bandera cambia qué código se acepta, qué análisis se ejecuta y qué información sobrevive hasta el binario. Compilar sin banderas no es “compilar limpio”: es apagar deliberadamente al único revisor que trabaja gratis en cada guardado.

🎯 Al terminar esta lección sabrás
  • Fijar el dialecto con -std=c23 y entender qué implica frente a gnu23.
  • Convertir los avisos en contrato con -Wall -Wextra -Werror.
  • Razonar la tensión entre -O0, -Og y -O2, y el papel real de -g.
  • Construir dos perfiles de compilación: desarrollo y publicación.

El dialecto: fijarlo siempre

El compilador siempre usa algún dialecto. Si no lo declaras, usas el que tu versión traiga por defecto, y ese valor cambia entre versiones: GCC 15 pasó a gnu23 mientras Clang seguía en gnu17. Un proyecto que no fija -std compila distinto en máquinas distintas, que es la peor clase de bug reproducible.

gcc -std=c23   prog.c -o prog   # C23 estricto, sin extensiones GNU
gcc -std=gnu23 prog.c -o prog   # C23 mas extensiones GNU

La diferencia no es cosmética. -std=c23 define __STRICT_ANSI__, desactiva las extensiones GNU y hace que las cabeceras del sistema expongan solo lo que el estándar promete. Elige c23 para código portable y gnu23 solo cuando dependas conscientemente de una extensión.

⚠️
El dialecto no es lo mismo que el aviso

-std=c23 fija qué dialecto se compila; -Wpedantic te avisa cuando lo abandonas. Sin -Wpedantic, ambos compiladores siguen aceptando en silencio bastantes extensiones aunque hayas pedido C estricto.

Avisos: de sugerencia a contrato

-Wall no es “todos los avisos”, pese al nombre: es el subconjunto que la comunidad considera indiscutible. -Wextra añade una segunda capa razonable. Y -Werror es la bandera que convierte el aviso de sugerencia en contrato: sin ella, un aviso es una línea que se pierde en el desplazamiento de la terminal.

gcc -std=c23 -Wall -Wextra -Wpedantic -Werror prog.c -o prog

Por encima del conjunto base hay una segunda tanda que no viene activada y que separa el código correcto del código robusto:

Bandera Qué caza
-Wconversion conversiones implícitas que pierden valor o signo
-Wshadow una variable local que tapa a otra del ámbito exterior
-Wvla arrays de longitud variable, opcionales en C23
-Wcast-qual conversiones que descartan const o volatile
-Wwrite-strings escritura sobre literales de cadena
-Wdouble-promotion promociones silenciosas de float a double

Ninguna de esas seis está en -Wall -Wextra, y todas cazan errores que compilan sin protestar. El caso de -Wconversion es especialmente doloroso porque la aritmética de enteros de C promociona en silencio:

void copiar(char *dst, const char *src, size_t n);

int  len = strlen(cadena);      /* size_t truncado a int */
char c   = 300;                 /* desbordamiento definido por implementacion */
copiar(dst, src, len - 1);      /* si len vale 0, n se vuelve enorme */

Las tres líneas compilan sin un solo aviso con -Wall -Wextra. Con -Wconversion las tres protestan, y la tercera es exactamente el patrón que produce desbordamientos de búfer explotables en código real.

📝
Dónde poner el -Werror

Actívalo siempre en integración continua. En desarrollo local resulta discutible: te bloquea a mitad de una exploración por una variable temporalmente sin usar. Muchos equipos maduros usan -Werror en CI y avisos normales en local, con la regla de que ningún aviso llega a la rama principal.

Optimización y depuración

-O no cambia solo la velocidad: cambia qué análisis ejecuta el compilador, y por tanto qué avisos puede emitir. Detecciones como -Wmaybe-uninitialized o -Wnull-dereference dependen del flujo de datos que se construye durante la optimización. Compilar con -O0 y esperar todos los avisos es una contradicción.

Nivel Uso real
-O0 traducción casi literal; depuración perfecta, avisos mínimos
-Og optimiza sin destruir la depuración; el nivel correcto para desarrollar
-O2 el estándar de publicación; equilibrio maduro entre tamaño y velocidad
-O3 vectorización y expansión agresivas; mide antes de asumir que gana
-Os y -Oz prioriza tamaño; habitual en sistemas embebidos

-g es ortogonal a todo lo anterior. Genera información DWARF —nombres de variables, tipos, correspondencia entre instrucción y línea— y no ralentiza el programa: solo engorda el fichero, y se puede separar después. Compilar la versión de publicación sin -g es la razón por la que tantos volcados de memoria de producción son ilegibles.

# -g3 conserva ademas las macros, para poder expandirlas en gdb
gcc -std=c23 -Og -g3 -fno-omit-frame-pointer prog.c -o prog

# publicar con simbolos, pero en un fichero aparte
gcc -std=c23 -O2 -g prog.c -o prog
objcopy --only-keep-debug prog prog.debug
objcopy --strip-debug --add-gnu-debuglink=prog.debug prog

Hay un corolario que sorprende a todo el mundo la primera vez: subir el nivel de optimización puede romper un programa que funcionaba. No porque el optimizador falle, sino porque a -O0 el comportamiento indefinido suele traducirse a algo inocuo, y a -O2 el compilador lo explota como premisa para reescribir el código.

int suma(int a, int b) {
    if (a + b < a)      /* asume que el desbordamiento con signo no ocurre */
        return -1;      /* a -O2 esta rama puede desaparecer entera */
    return a + b;
}

Un binario que solo falla en publicación casi nunca es culpa del compilador: es código indefinido al que le retiraron la red.

-fno-omit-frame-pointer merece su propia frase: sin él, -O2 reutiliza el registro del puntero de marco y los perfiladores y depuradores pierden la capacidad de reconstruir la pila. El coste en rendimiento es de un pequeño porcentaje; el beneficio en diagnosticabilidad es total.

El conjunto mínimo profesional

flowchart TD
A[Para que compilo este binario] --> B[Desarrollo y depuracion]
A --> C[Publicacion]
B --> B1[-std=c23 -Og -g3]
B --> B2[-Wall -Wextra -Wpedantic -Werror]
B --> B3[-fno-omit-frame-pointer]
C --> C1[-std=c23 -O2 -g -DNDEBUG]
C --> C2[-D_FORTIFY_SOURCE=3 -fstack-protector-strong]
C --> C3[-Wl,-z,relro,-z,now]
style B1 fill:#a6e3a1,color:#11111b
style C1 fill:#89b4fa,color:#11111b
# perfil de desarrollo
CFLAGS_DEV="-std=c23 -Og -g3 -fno-omit-frame-pointer \
            -Wall -Wextra -Wpedantic -Werror \
            -Wconversion -Wshadow -Wvla"

# perfil de publicacion endurecido
CFLAGS_REL="-std=c23 -O2 -g -DNDEBUG \
            -Wall -Wextra \
            -D_FORTIFY_SOURCE=3 -fstack-protector-strong \
            -fno-omit-frame-pointer"

gcc $CFLAGS_DEV prog.c -o prog_dev
gcc $CFLAGS_REL prog.c -o prog_rel

Escribir esto a mano cada vez es insostenible: en los niveles de sistemas de construcción estas cadenas viven una sola vez en el fichero de configuración del proyecto. Lo que no cambia es la decisión de fondo, y esa la tomas aquí.

Las banderas son la especificación ejecutable de tu contrato con la máquina

Se enseñan las banderas como una lista de conjuros, y así se olvidan. Míralas como lo que son: la declaración formal de qué promesas haces y qué garantías exiges. -std=c23 declara contra qué semántica escribes. -Werror declara que un aviso es un defecto, no una opinión. -O2 declara que autorizas al compilador a reescribir tu programa siempre que preserve el comportamiento observable —y ahí está la trampa, porque “observable” se define solo para programas sin comportamiento indefinido—. Por eso un bug que aparece al subir de -O0 a -O2 casi nunca es del optimizador: es tu código incumpliendo un contrato que firmaste sin leer, y que en -O0 nadie te reclamó. Y -g es la declaración de que quieres poder responder a la pregunta “qué pasó” cuando el binario falle a las tres de la mañana en una máquina que no es la tuya. Las banderas no configuran el compilador: configuran cuánta verdad estás dispuesto a que te digan.

⚔️ Construye tus dos perfiles
  1. Compila un programa con -O0 y luego con -O2, ambos con -Wall -Wextra, y compara los avisos. Explica por qué aparecen más en el segundo.
  2. Escribe una función que lea una variable local sin inicializar y comprueba a partir de qué nivel de optimización lo detecta cada compilador.
  3. Activa -Wconversion sobre un proyecto tuyo. Cuenta cuántos avisos salen y arregla los cinco primeros.
  4. Genera un binario con -O2 -g, separa los símbolos con objcopy y verifica que el depurador sigue resolviendo nombres de función.
  5. Define tus propias variables CFLAGS_DEV y CFLAGS_REL y guárdalas donde puedas reutilizarlas en cada proyecto nuevo.