GNU/Linux >> Tutoriales Linux >  >> Linux

Viaje de un programa en C a un ejecutable de Linux en 4 etapas

Usted escribe un programa en C, usa gcc para compilarlo y obtiene un ejecutable. Es bastante simple. ¿Verdad?

¿Alguna vez te has preguntado qué sucede durante el proceso de compilación y cómo se convierte el programa C en un ejecutable?

Hay cuatro etapas principales por las que pasa un código fuente para convertirse finalmente en un ejecutable.

Las cuatro etapas para que un programa en C se convierta en un ejecutable son las siguientes:

  1. Preprocesamiento
  2. Compilación
  3. Montaje
  4. Enlace

En la Parte I de esta serie de artículos, analizaremos los pasos que sigue el compilador gcc cuando el código fuente de un programa C se compila en un ejecutable.

Antes de continuar, echemos un vistazo rápido a cómo compilar y ejecutar un código 'C' usando gcc, usando un ejemplo simple de hola mundo.

$ vi print.c
#include <stdio.h>
#define STRING "Hello World"
int main(void)
{
/* Using a macro to print 'Hello World'*/
printf(STRING);
return 0;
}

Ahora, ejecutemos el compilador gcc sobre este código fuente para crear el ejecutable.

$ gcc -Wall print.c -o print

En el comando anterior:

  • gcc:invoca el compilador GNU C
  • -Wall:bandera gcc que habilita todas las advertencias. -W significa advertencia, y estamos pasando "todos" a -W.
  • print.c:programa de entrada C
  • -o print:indica al compilador de C que cree el ejecutable de C como print. Si no especifica -o, por defecto el compilador de C creará el ejecutable con el nombre a.out

Finalmente, ejecute print que ejecutará el programa C y mostrará hola mundo.

$ ./print
Hello World

Nota :Cuando esté trabajando en un gran proyecto que contenga varios programas en C, use la utilidad make para administrar la compilación de su programa en C, como comentamos anteriormente.

Ahora que tenemos una idea básica sobre cómo se usa gcc para convertir un código fuente en binario, revisaremos las 4 etapas por las que debe pasar un programa C para convertirse en un ejecutable.

1. PREPROCESAMIENTO

Esta es la primera etapa por la que pasa un código fuente. En esta etapa se realizan las siguientes tareas:

  1. Sustitución de macros
  2. Los comentarios se eliminan
  3. Expansión de los archivos incluidos

Para comprender mejor el preprocesamiento, puede compilar el programa 'print.c' anterior usando flag -E, que imprimirá la salida preprocesada en stdout.

$ gcc -Wall -E print.c

Aún mejor, puede usar la marca '-save-temps' como se muestra a continuación. El indicador '-save-temps' indica al compilador que almacene los archivos intermedios temporales utilizados por el compilador gcc en el directorio actual.

$ gcc -Wall -save-temps print.c -o print

Entonces, cuando compilamos el programa print.c con el indicador -save-temps, obtenemos los siguientes archivos intermedios en el directorio actual (junto con el ejecutable de impresión)

$ ls
print.i
print.s
print.o

La salida preprocesada se almacena en el archivo temporal que tiene la extensión .i (es decir, 'print.i' en este ejemplo)

Ahora, abramos el archivo print.i y veamos el contenido.

$ vi print.i
......
......
......
......
# 846 "/usr/include/stdio.h" 3 4
extern FILE *popen (__const char *__command, __const char *__modes) ;
extern int pclose (FILE *__stream);
extern char *ctermid (char *__s) __attribute__ ((__nothrow__));

# 886 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__));

# 916 "/usr/include/stdio.h" 3 4
# 2 "print.c" 2

int main(void)
{
printf("Hello World");
return 0;
}

En el resultado anterior, puede ver que el archivo fuente ahora está lleno de mucha, mucha información, pero aún al final podemos ver las líneas de código escritas por nosotros. Primero analicemos estas líneas de código.

  1. La primera observación es que el argumento de printf() ahora contiene directamente la cadena "Hello World" en lugar de la macro. De hecho, la definición y el uso de macros han desaparecido por completo. Esto prueba la primera tarea de que todas las macros se expanden en la etapa de preprocesamiento.
  2. La segunda observación es que el comentario que escribimos en nuestro código original no está ahí. Esto prueba que se eliminaron todos los comentarios.
  3. La tercera observación es que al lado de la línea "#include" falta y en su lugar vemos mucho código en su lugar. Por lo tanto, es seguro concluir que stdio.h se ha expandido y se ha incluido literalmente en nuestro archivo fuente. Por lo tanto, entendemos cómo el compilador puede ver la declaración de la función printf().

Cuando busqué el archivo print.i, encontré que la función printf se declara como:

extern int printf (__const char *__restrict __format, ...);

La palabra clave 'extern' indica que la función printf() no está definida aquí. Es externo a este archivo. Más adelante veremos cómo llega gcc a la definición de printf().

Puede usar gdb para depurar sus programas c. Ahora que tenemos una comprensión decente de lo que sucede durante la etapa de preprocesamiento. pasemos a la siguiente etapa.

2. COMPILACIÓN

Después de que el compilador termine con la etapa de preprocesador. El siguiente paso es tomar print.i como entrada, compilarlo y producir una salida compilada intermedia. El archivo de salida para esta etapa es 'print.s'. El resultado presente en print.s son instrucciones de nivel de ensamblaje.

Abra el archivo print.s en un editor y vea el contenido.

$ vi print.s
.file "print.c"
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
movq %rsp, %rbp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
movl $.LC0, %eax
movq %rax, %rdi
movl $0, %eax
call printf
movl $0, %eax
leave
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits

Aunque no me gusta mucho la programación a nivel de ensamblador, una mirada rápida concluye que esta salida a nivel de ensamblador tiene algún tipo de instrucciones que el ensamblador puede entender y convertir a lenguaje de nivel de máquina.

3. MONTAJE

En esta etapa se toma como entrada el archivo print.s y se produce un archivo intermedio print.o. Este archivo también se conoce como archivo de objeto.

Este archivo es producido por el ensamblador que comprende y convierte un archivo '.s' con instrucciones de ensamblaje en un archivo de objeto '.o' que contiene instrucciones a nivel de máquina. En esta etapa, solo el código existente se convierte en lenguaje de máquina, las llamadas a funciones como printf() no se resuelven.

Dado que la salida de esta etapa es un archivo de nivel de máquina (print.o). Así que no podemos ver el contenido de la misma. Si aún intenta abrir print.o y verlo, verá algo que no se puede leer en absoluto.

$ vi print.o
^?ELF^B^A^A^@^@^@^@^@^@^@^@^@^A^@>^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@0^
^@UH<89>å¸^@^@^@^@H<89>ǸHello World^@^@GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3^@^
T^@^@^@^@^@^@^@^AzR^@^Ax^P^A^[^L^G^H<90>^A^@^@^\^@^@]^@^@^@^@A^N^PC<86>^B^M^F
^@^@^@^@^@^@^@^@.symtab^@.strtab^@.shstrtab^@.rela.text^@.data^@.bss^@.rodata
^@.comment^@.note.GNU-stack^@.rela.eh_frame^@^@^@^@^@^@^@^@^@^@^@^
...
...
…

Lo único que podemos explicar mirando el archivo print.o es sobre la cadena ELF.

ELF significa formato ejecutable y enlazable.

Este es un formato relativamente nuevo para archivos de objetos a nivel de máquina y ejecutables producidos por gcc. Antes de esto, se usaba un formato conocido como a.out. Se dice que ELF es un formato más sofisticado que a.out (podríamos profundizar en el formato ELF en algún otro artículo futuro).

Nota:si compila su código sin especificar el nombre del archivo de salida, el archivo de salida producido tiene el nombre 'a.out' pero el formato ahora ha cambiado a ELF. Es solo que el nombre del archivo ejecutable predeterminado sigue siendo el mismo.

4. ENLACE

Esta es la etapa final en la que se realiza toda la vinculación de las llamadas a funciones con sus definiciones. Como se discutió anteriormente, hasta esta etapa gcc no conoce la definición de funciones como printf(). Hasta que el compilador sepa exactamente dónde se implementan todas estas funciones, simplemente usa un marcador de posición para la llamada de función. Es en esta etapa, se resuelve la definición de printf() y se conecta la dirección real de la función printf().

El enlazador entra en acción en esta etapa y realiza esta tarea.

El enlazador también realiza un trabajo extra; combina algún código adicional a nuestro programa que se requiere cuando el programa comienza y cuando finaliza. Por ejemplo, hay un código que es estándar para configurar el entorno de ejecución, como pasar argumentos de línea de comandos, pasar variables de entorno a cada programa. De manera similar, algún código estándar que se requiere para devolver el valor de retorno del programa al sistema.

Las tareas anteriores del compilador se pueden verificar mediante un pequeño experimento. Desde ahora ya sabemos que el enlazador convierte el archivo .o (print.o) en un archivo ejecutable (print).

Entonces, si comparamos los tamaños de archivo de los archivos print.o e print, veremos la diferencia.

$ size print.o
   text	   data	    bss	    dec	    hex	filename
     97	      0	      0	     97	     61	print.o 

$ size print
   text	   data	    bss	    dec	    hex	filename
   1181	    520	     16	   1717	    6b5	print

A través del comando de tamaño, obtenemos una idea aproximada de cómo aumenta el tamaño del archivo de salida de un archivo de objeto a un archivo ejecutable. Todo esto se debe a ese código estándar adicional que el enlazador combina con nuestro programa.

Ahora ya sabe lo que le sucede a un programa en C antes de que se convierta en un ejecutable. Ya conoce las etapas de preprocesamiento, compilación, ensamblaje y vinculación. Hay mucho más en la etapa de vinculación, que cubriremos en el próximo artículo de esta serie.


Linux
  1. Ejemplos de comandos awk en Linux

  2. Ejemplos de comandos lpr en Linux

  3. Incrustar un icono en un ejecutable de Linux

  4. Instalar un programa de Python en Linux

  5. ¿Puede obtener algún programa en Linux para imprimir un seguimiento de la pila si falla?

Cómo hacer un archivo ejecutable en Linux

5 consejos para dominar Linux

Restaurar permiso ejecutable al comando Chmod en Linux

Imprimir el tiempo de ejecución de la secuencia de comandos de Shell en Linux

Maneras brillantes de cómo ejecutar un programa en Linux

Cómo escribir y ejecutar un programa C en Linux