Al compilar un archivo de lenguaje C con la opción -v podemos descubrir los detalles de las herramientas que son invocadas durante el proceso de construcción de un binario.

gcc -v hw.c

Algunas partes importantes:

El compilador se llama cc1. El producto de esta etapa es un archivo de código assembly por cada archivo *.c:

/usr/lib/gcc/x86_64-linux-gnu/9/cc1 -quiet -v -imultiarch x86_64-linux-gnu hw.c -quiet -dumpbase hw.c -mtune=generic -march=x86-64 s

El ensamblador se llama as. El producto de esta etapa es un object file por cada archivo de assembly:

as -v --64 -o /tmp/cceE5FNa.o /tmp/cclaDJ69.s

El programa collect2 se encarga del proceso de linking, i.e. combinar los object files en un archivo ejecutable:

/usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-lo

Veamos partes de los productos intermedios y final del proceso de contrucción de binario.

(antes de compilar) El preprocesador

El preprocesador de C atiende los preprocesor directives (en C, las líneas que comienzan con #). Esas directivas se pueden usar, entre otras cosas:

#include<stdio.h>

El preprocesador sustituye esa línea por el contenido completo del archivo stdio.h.

#define CMXNPULGADA 2.54 #define mayor(a,b) a > b ? a : b . . . . . . cm = CMXPULGADA * pulgadas; x = mayor(p,q)

Preprocesador convierte en esto:

cm = 2.54 * pulgadas; x = p > q ? p ; q;

Compilación (producto = assembly code)

A continuación, parte del assembly generado para un Hello World. Nota el string "Hello World" y más adelante las instrucciones que cargan la dirección de ese string al stack para luego invocar la subrutina printf@PLT.

.LC0:
    .string "Hello, World!"
  ...
  ...
    leal    .LC0@GOTOFF(%eax), %edx
    pushl   %edx
    movl    %eax, %ebx
    call    printf@PLT

Ensamblaje (producto = object file)

El ensamblador convierte las instrucciones de lenguaje de ensamblaje a lenguaje de máquina y genera un archivo binario (no ejecutable) donde organiza las instrucciones, datos y otras partes necesarias para que el archivo pueda ser combinado con otros y formar un ejecutable.

A continuación parte del object file que se creó para el programa hw.c. Observa que en este punto, como no se sabe donde estará implementada la función printf, solo se identifica su llamada con un número call 0x2a. En esta etapa tampoco se sabe dónde quedarán los datos (como el string "Hello World") lea 0x0(%eax), %edx.

...
  20:   8d 90 00 00 00 00       lea    0x0(%eax),%edx   <=(b)===
  26:   52                      push   %edx
  27:   89 c3                   mov    %eax,%ebx
  29:   e8 fc ff ff ff          call   2a <main+0x2a>   <=(a)===
  2e:   83 c4 10                add    $0x10,%esp
  31:   b8 00 00 00 00          mov    $0x0,%eax
  36:   8d 65 f8                lea    -0x8(%ebp),%esp
...

Linker (producto = ejecutable)

....
 80491b6:   8d 90 08 e0 ff ff       lea    -0x1ff8(%eax),%edx
 80491bc:   52                      push   %edx
 80491bd:   89 c3                   mov    %eax,%ebx
 80491bf:   e8 9c fe ff ff          call   8049060 <printf@plt>
 80491c4:   83 c4 10                add    $0x10,%esp
 80491c7:   b8 00 00 00 00          mov    $0x0,%eax
 80491cc:   8d 65 f8                lea    -0x8(%ebp),%esp
...

Cuando pides que se ejecute el binario, el programa loader del sistema operativo carga tu programa a memoria y lo prepara para ser ejecutado. Cosas como: (0) separar la memoria necesaria para el proceso (1) copiar las diferentes partes del ejecutable a la partes de la memoria que le serán asignadas al proceso, (1.5) resolver referencias que hace el programa a librerías externas (2) proveer los argumentos de línea de commando, (3) inicializar los registros en preparación para la ejecución (e.g. stack pointer y otros), (4) brincar al punto de entrada del ejecutable.

Para comenzar a enteder algunas de las cosas que hace el linker, fíjate en la abal de símbolos al final del archivo ejecutable de un programa Hello World.

...
    51: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.2.5
    52: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
....
    58: 0000000000004018     0 NOTYPE  GLOBAL DEFAULT   26 _end
    59: 0000000000001060    47 FUNC    GLOBAL DEFAULT   16 _start
    60: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    61: 0000000000001149    32 FUNC    GLOBAL DEFAULT   16 main
...

Observa como, entre otras, aparecen la función main y la función printf@GLIBC_2.2.5 (aka printf). La columna previa al nombre de estas dos funciones dice 16 para main mientras que dice UND para printf:

Una de las labores que realiza el loader al cargar nuestro programa a memoría es resolver la dirección de printf para que nuestro ejecutable sepa donde se encuentra la implementación de esa función en la librería compartida.