C desde la línea de comandos (parte 1)

gcc y clang aceptan una serie de parámetros que pueden darse desde una línea de comandos en una terminal para obtener resultados más precisos de cara a compilar manualmente un archivo o a manipular la cadena de compilación. En este episodio os cuento los distintos parámetros que podemos aplicar para compilar sin IDE.

Cuando compilamos un programa en C con GCC, el proceso que sigue el compilador es bastante interesante y se divide en varias etapas que nos permiten entender qué ocurre desde que escribimos el código hasta que obtenemos un ejecutable. Empecemos por la primera fase: el preprocesado.

El preprocesador es un programa independiente dentro del ecosistema de GCC, conocido como CPP (C PreProcessor). Su función principal es preparar el código fuente para que el compilador pueda trabajar con él de forma más sencilla. Esto implica eliminar comentarios, reducir espacios innecesarios y, sobre todo, resolver directivas como #include. Por ejemplo, cuando incluimos #include <stdio.h>, el preprocesador sustituye esa línea por el contenido literal del archivo stdio.h, lo que permite que el compilador conozca las definiciones y declaraciones necesarias para funciones como printf.

Podemos invocar directamente el preprocesador con el comando cpp, que recibe el código fuente por la entrada estándar y emite el código preprocesado por la salida estándar. Esto nos permite ver cómo queda el código tras esta primera transformación. También GCC ofrece una forma más sencilla de obtener este resultado usando la opción -E, que hace que el compilador realice solo el preprocesado y nos muestre el resultado sin continuar con las siguientes etapas.

Una vez que el código está preprocesado, GCC lo traduce a lenguaje ensamblador. Esta es la siguiente fase del proceso y se puede observar usando la opción -S al invocar GCC. El archivo resultante tiene extensión .s y contiene el código ensamblador equivalente a nuestro programa en C. Este código es específico para la arquitectura y el sistema operativo en el que estemos trabajando, por lo que puede variar considerablemente.

Por ejemplo, en una máquina con arquitectura x86, el ensamblador generado mostrará ciertas instrucciones y convenciones propias de esa plataforma. En cambio, si compilamos el mismo código en una Raspberry Pi, que utiliza arquitectura ARM, el código ensamblador será completamente distinto, con instrucciones y sintaxis propias de ARM. Esto refleja cómo GCC adapta el código a la plataforma destino.

Además, GCC realiza optimizaciones durante esta traducción. Un caso típico es la sustitución de llamadas a funciones estándar por otras más eficientes. Por ejemplo, en lugar de llamar a printf, el compilador puede reemplazarla por puts cuando detecta que la llamada es simple y no requiere formateo complejo, lo que ahorra recursos y tiempo de ejecución.

En sistemas BSD como macOS, al usar compiladores compatibles con GCC o LLVM (como Clang), el proceso es similar, aunque el código ensamblador generado también será específico para esa plataforma. En este caso, puede que no se realicen ciertas optimizaciones como la sustitución de printf por puts, dependiendo de cómo esté configurado el compilador y las características del sistema.

En resumen, entender estas etapas nos ayuda a tener una visión más clara de lo que ocurre bajo el capó cuando compilamos un programa en C. Desde el preprocesado que prepara el código, pasando por la generación de ensamblador adaptado a la plataforma, hasta las optimizaciones que hacen que nuestro programa sea más eficiente. Esto es fundamental para quienes quieren profundizar en la compilación y el funcionamiento interno de los programas en C.

Para ilustrar cómo invocar estas etapas, podemos ver algunos comandos útiles:

# Preprocesar un archivo y mostrar el resultado
gcc -E archivo.c

# Generar código ensamblador a partir del código fuente
gcc -S archivo.c

El archivo .s resultante contendrá el código ensamblador que luego será ensamblado y enlazado para crear el ejecutable final. Explorar este código nos permite entender mejor cómo el compilador interpreta y transforma nuestro código C en instrucciones que la máquina puede ejecutar.

Lista de reproducción
  1. 1
    Instalar CodeBlocks
    15 minutos
  2. 2
    Funciones y hola mundo
    17 minutos
  3. 3
    Variables y tipos de datos
    17 minutos
  4. 4
    Condicionales y operadores lógicos
    16 minutos
  5. 5
    Bucles
    11 minutos
  6. 6
    Punteros
    12 minutos
  7. 7
    Arrays
    14 minutos
  8. 8
    Estructuras
    12 minutos
  9. 9
    Otras construcciones de C
    9 minutos
  10. 10
    Memoria dinámica
    8 minutos
  11. 11
    El preprocesador (parte 1)
    10 minutos
  12. 12
    El preprocesador (parte 2)
    9 minutos
  13. 13
    Archivos de cabecera y múltiples .c (parte 1)
    8 minutos
  14. 14
    Archivos de cabecera y múltiples .c (parte 2)
    9 minutos
  15. 15
    C desde la línea de comandos (parte 1)
    8 minutos
  16. 16
    C desde la línea de comandos (parte 2)
    9 minutos
  17. 17
    Break y continue
    10 minutos
  18. 18
    Goto
    13 minutos
  19. 19
    Manipulación de bits
    15 minutos
  20. 20
    Máscaras de bit
    19 minutos
  21. 21
    Archivos (1): fopen y fclose
    13 minutos
  22. 22
    Archivos (2): leer con fgetc
    9 minutos
  23. 23
    Archivos (3): fseek y ftell
    11 minutos
  24. 24
    Archivos (4): leer con fgets
    9 minutos
  25. 25
    Archivos (5): fputc y fputs
    7 minutos
  26. 26
    Archivos (6): volcar en archivos con fwrite
    10 minutos
  27. 27
    Archivos (7): fread, fwrite y los arrays
    10 minutos
  28. 28
    Archivos (8): entrada estándar y salida estándar
    9 minutos
  29. 29
    Archivos (9): buffers
    14 minutos
  30. 30
    Archivos (y 10): otras funciones útiles con archivos
    5 minutos
  31. 31
    printf (1)
    18 minutos
  32. 32
    printf (parte 2)
    12 minutos
  33. 33
    scanf (parte 1)
    17 minutos
  34. 34
    scanf (parte 2)
    17 minutos
  35. 35
    fprintf, sprintf y snprintf
    8 minutos
  36. 36
    Tipos de datos opacos
    13 minutos
  37. 37
    Bibliotecas estáticas
    13 minutos
  38. 38
    Bibliotecas dinámicas
    15 minutos
  39. 39
    Más flags: i mayúscula (include), wall, werror, pedantic...
    12 minutos
  40. 40
    pkg-config
    12 minutos
  41. 41
    Make
    17 minutos
  42. 42
    GDB
    21 minutos
  43. 43
    Variables globales
    6 minutos
  44. 44
    extern
    9 minutos
  45. 45
    Funciones variádicas
    12 minutos
  46. 46
    El optimizador de GCC y la opción -O
    12 minutos
  47. 47
    Volatile
    6 minutos