Ir al contenido principal

Compilación en C a detalle: de main.c a main.exe

El proceso de compilación en C comprende de una serie de pasos entre que el codigo fuente (.c y .h) se integran y unen para crear un ejecutable (Un .exe en Windows o un ELF en Linux).

El código fuente es un programa escrito en algún lenguaje de programación, un conjunto de líneas de texto que debe seguir y ejecutar la computadora para cumplir el objetivo para el que fue creado. Pero antes de ser ejecutado este tiene que atravesar una serie de fases hasta convertirse en la unidad mínima procesable por la computadora, el código maquina.


En el lenguaje de programación C este proceso se comprende en una serie de fases que vamos a ir analizando en detalle el proceso las opciones que nos ofrece el compilador para observar el proceso paso a paso hasta obtener el ejecutable.

En los ejemplos voy a estar usando el compilador GCC sobre Windows y el editor de texto Sublime Text 4.

Proceso de compilación en C

Preprocesado -> Compilación -> Ensamblaje -> Enlazamiento

* Compilación es un paso del proceso, pero se suele denominar indistintamente el proceso completo como compilación.

Preprocesado

En esta fase, el compilador se encarga de reunir todo el código fuente incluido el de las dependencias.

El flag "-E" en gcc nos permite solo pre-procesar el codigo fuente y guardarlo en la salida con el flag "-o" de output seguido del nombre.

Como podemos observar, todo el contenido de la librería "stdio.h" así como las dependencias de sus dependencias en forma recursiva, se ha reunido en un solo código fuente..

Compilación

Ahora que están todas las condiciones reunidas, el compilador puede compilar el código fuente, el resultado no es un ejecutable, si no un código ensamblador. El assembler es un lenguaje de programación que pretende abstraer el proceso de tener que escribir directamente en código maquina de una forma legible y portable. 

gcc nos permite obtener el ASM usando el flag -S.

Ensamblaje

En este paso, el compilador va a tomar todo ese código en ensamblador y lo va a convertir a binario puro, en código maquina, también conocido como código objeto, una serie de instrucciones que puede interpretar el procesador directamente.

Con el flag "-c" gcc nos permite generar el código objeto

Enlazado

Este es el paso final, el compilador tomará el código objeto (o los), lo enlazará y juntará en un solo fichero con las condiciones necesarias para ser ejecutado. El enlazado no se trata solamente de un rejunte si no que se terminan por resolver todas las posibles referencias y vínculos asociándolos al sistema operativo en el que esté corriendo.

Obtenemos del código objeto el ejecutable final.

Nótese que en las ilustraciones fuimos usando las distintas salidas de cada proceso para dar con el resultado final de forma manual.


Tipos de enlazados

El enlazado se trata de resolver hechos y hacerlos encajar entre sí para dar paso a la ejecución del programa, pero puede haber casos en los que no queramos enlazar de todo el programa para crear librerías, ya sea para compartirlas con otros programas o consumidores.

Para esto el compilador nos permite resolver los enlaces de dos maneras, estáticamente o dinámicamente.

Enlace estático / Librería estática

En la compilación que hicimos al principio, el enlazador compiló y enlazó todo en un solo ejecutable, este tipo de enlace reúne todas las condiciones estáticamente, todo lo que necesita el código para ejecutarse está ya acomodado desde que se compiló en un único ejecutable.

Si necesita ejecutar una función, ya cuenta con el código de esta en su interior.



Enlace dinámico / Librería dinámica

Al contrario del estático, las funciones de la librería que nuestro código quiera ejecutar se encontraran apartadas en una librería de enlace dinámico  (El famoso .dll en Windows o .so en Unix), el ejecutable se enlazará dinámicamente en tiempo de ejecución resolviendo las referencias y obteniendo de ella lo que necesite.



Crear una librería dinámica

Veamos un caso practico, creemos nuestra propia librería, esta librería permite crear interacciones sociales como saludar, despedir o llamarle la atención a alguien.

A la derecha, la interfaz, a la izquierda la implementación.


Procedemos a ensamblar, obtener el código objeto.
gcc -c <codigo_fuente>


Ahora en vez de hacerlo un ejecutable, lo dejamos enlazado "a medias".
gcc <codigo_objeto> -shared -o <nuevo_dll>

Listo, ahora tenemos en nuestras manos una librería dinámica. Ahora supongamos que queremos usarlo en un desarrollo que estemos haciendo, para poder incluirlo e invocar funciones en nuestro programa no alcanza solamente con el .dll, si no que precisaremos el header con las declaraciones (.h). Este header actuaría como una especie de "indice" de nuestra librería, con todas las declaraciones asociadas a ella.

Incluimos el header y usamos las funciones de la librería.

Lo compilamos

gcc -L<directorio_libs> -l<libreria.dll> -o <nombre_ejecutable>


Y listo, ya tenemos nuestro ejecutable, y corre perfectamente!


Si removemos el .dll del directorio e intentamos ejecutar veremos un error pidiendo la librería.


Las librerías de permite modularizar de cierta forma un programa o crear librerías que otros programadores o programas podrían usar y compartir tranquilamente (Por ejemplo una API), además de reducir el tamaño del ejecutable considerablemente y también ahorrarnos el trabajo de tener que andar recopilando código fuente reiteradas veces (algo de lo que se aprovecha la herramienta make).



¿Qué diferencia hay entre una librería estática y una dinámica?

La librería estática básicamente es el código objeto, podemos tener muchos .o y terminar por enlazarlos creando el ejecutable final, si una librería estática resulta modificada, hay que re enlazar todo nuevamente.

Mientras que en una librería dinámica si la implementación por algún motivo cambia, solo tenemos que actualizar la librería dinámica y no necesariamente enlazar todo otra vez, aparte pueden rejuntar varios códigos objetos en un solo lugar. ¡Una verdadera librería!.


Si quisiéramos compilar con enlace estático, es decir, todo dentro del mismo ejecutable deberíamos entonces agregar el parámetro "-static" en la compilación. Esto evitaría tener que depender explícitamente de un dll dinámico.


Conclusión

Como programador siempre es bueno conocer que esconde el proceso de compilación y también nos permite comprender como se asocia este a el funcionamiento de la mayoría de sistemas que esconden en sus entrañas C/C++.





Y eso es todo, espero que les haya sido claro y servido para comprender un poco más acerca de este maravilloso mundo. Saludos!