¿Cómo funciona el proceso de compilación / enlace?

417

¿Cómo funciona el proceso de compilación y enlace?

(Nota: Esto está destinado a ser una entrada a las preguntas frecuentes de C ++ de Stack Overflow . Si desea criticar la idea de proporcionar preguntas frecuentes en este formulario, entonces la publicación en meta que comenzó todo esto sería el lugar para hacerlo. Respuestas a esa pregunta se monitorea en la sala de chat de C ++ , donde la idea de preguntas frecuentes comenzó en primer lugar, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea).

desconocido
fuente

Respuestas:

555

La compilación de un programa C ++ implica tres pasos:

  1. Preprocesamiento: el preprocesador toma un archivo de código fuente C ++ y se ocupa de las directivas #includes, #definesy otras preprocesadores. El resultado de este paso es un archivo C ++ "puro" sin directivas de preprocesador.

  2. Compilación: el compilador toma la salida del preprocesador y produce un archivo objeto a partir de ella.

  3. Vinculación: el vinculador toma los archivos de objetos producidos por el compilador y produce una biblioteca o un archivo ejecutable.

Preprocesamiento

El preprocesador maneja las directivas del preprocesador , como #includey #define. Es independiente de la sintaxis de C ++, por lo que debe usarse con cuidado.

Funciona en un archivo C ++ fuente a la vez mediante la sustitución de #includedirectivas con el contenido de los archivos respectivos (que suele ser sólo declaraciones), haciendo la sustitución de macros ( #define), y la selección de diferentes partes del texto en función de #if, #ifdefy #ifndefdirectivas.

El preprocesador funciona en una secuencia de tokens de preprocesamiento. La sustitución de macros se define como el reemplazo de tokens por otros tokens (el operador ##permite fusionar dos tokens cuando tiene sentido).

Después de todo esto, el preprocesador produce una única salida que es una secuencia de tokens que resultan de las transformaciones descritas anteriormente. También agrega algunos marcadores especiales que le dicen al compilador de dónde provino cada línea para que pueda usarlos para producir mensajes de error sensibles.

Se pueden producir algunos errores en esta etapa con un uso inteligente de las directivas #ify #error.

Compilacion

El paso de compilación se realiza en cada salida del preprocesador. El compilador analiza el código fuente puro de C ++ (ahora sin directivas de preprocesador) y lo convierte en código ensamblador. Luego invoca el back-end subyacente (ensamblador en la cadena de herramientas) que ensambla ese código en código de máquina produciendo un archivo binario real en algún formato (ELF, COFF, a.out, ...). Este archivo de objeto contiene el código compilado (en forma binaria) de los símbolos definidos en la entrada. Los símbolos en los archivos de objetos se denominan por su nombre.

Los archivos de objetos pueden referirse a símbolos que no están definidos. Este es el caso cuando utiliza una declaración y no proporciona una definición para ella. Al compilador no le importa esto, y felizmente producirá el archivo objeto siempre que el código fuente esté bien formado.

Los compiladores generalmente le permiten detener la compilación en este punto. Esto es muy útil porque con él puede compilar cada archivo de código fuente por separado. La ventaja que esto proporciona es que no necesita volver a compilar todo si solo cambia un solo archivo.

Los archivos de objetos producidos pueden colocarse en archivos especiales llamados bibliotecas estáticas, para facilitar su reutilización más adelante.

Es en esta etapa que se informan errores de compilador "regulares", como errores de sintaxis o errores de resolución de sobrecarga fallidos.

Enlace

El enlazador es lo que produce la salida de compilación final de los archivos de objetos que produjo el compilador. Este resultado puede ser una biblioteca compartida (o dinámica) (y aunque el nombre es similar, no tienen mucho en común con las bibliotecas estáticas mencionadas anteriormente) o un ejecutable.

Vincula todos los archivos de objetos reemplazando las referencias a símbolos indefinidos con las direcciones correctas. Cada uno de estos símbolos se puede definir en otros archivos de objetos o en bibliotecas. Si se definen en bibliotecas distintas de la biblioteca estándar, debe informar al vinculador sobre ellas.

En esta etapa, los errores más comunes son definiciones faltantes o definiciones duplicadas. Lo primero significa que las definiciones no existen (es decir, no están escritas) o que los archivos de objetos o las bibliotecas donde residen no se entregaron al vinculador. Esto último es obvio: el mismo símbolo se definió en dos archivos de objetos o bibliotecas diferentes.

R. Martinho Fernandes
fuente
39
La etapa de compilación también llama al ensamblador antes de convertirlo a un archivo objeto.
manav mn
3
¿Dónde se aplican las optimizaciones? A primera vista, parece que se haría en el paso de compilación, pero por otro lado, puedo imaginar que la optimización adecuada solo se puede hacer después de la vinculación.
Bart van Heukelom
66
@BartvanHeukelom tradicionalmente se realizaba durante la compilación, pero los compiladores modernos admiten la llamada "optimización del tiempo de enlace", que tiene la ventaja de poder optimizar en todas las unidades de traducción.
R. Martinho Fernandes
3
¿C tiene los mismos pasos?
Kevin Zhu
66
Si el enlazador convierte símbolos que se refieren a clases / métodos en bibliotecas en direcciones, ¿eso significa que los binarios de la biblioteca se almacenan en direcciones de memoria que el sistema operativo mantiene constante? Estoy confundido sobre cómo el enlazador conocería la dirección exacta de, digamos, el binario estándar para todos los sistemas de destino. La ruta del archivo siempre sería la misma, pero la dirección exacta puede cambiar, ¿verdad?
Dan Carter
42

Este tema se trata en CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html

Aquí está lo que escribió el autor:

¡Compilar no es lo mismo que crear un archivo ejecutable! En cambio, crear un ejecutable es un proceso de varias etapas dividido en dos componentes: compilación y vinculación. En realidad, incluso si un programa "compila bien", podría no funcionar debido a errores durante la fase de enlace. El proceso total de pasar de archivos de código fuente a un ejecutable podría ser mejor conocido como una compilación.

Compilacion

La compilación se refiere al procesamiento de archivos de código fuente (.c, .cc o .cpp) y la creación de un archivo 'objeto'. Este paso no crea nada que el usuario pueda ejecutar realmente. En cambio, el compilador simplemente produce las instrucciones de lenguaje de máquina que corresponden al archivo de código fuente que se compiló. Por ejemplo, si compila (pero no vincula) tres archivos separados, tendrá tres archivos de objeto creados como salida, cada uno con el nombre .o o .obj (la extensión dependerá de su compilador). Cada uno de estos archivos contiene una traducción de su archivo de código fuente a un archivo de lenguaje de máquina, ¡pero aún no puede ejecutarlos! Debe convertirlos en ejecutables que su sistema operativo pueda usar. Ahí es donde entra el enlazador.

Enlace

La vinculación se refiere a la creación de un único archivo ejecutable a partir de múltiples archivos de objetos. En este paso, es común que el enlazador se queje de funciones indefinidas (comúnmente, main). Durante la compilación, si el compilador no pudo encontrar la definición de una función en particular, simplemente supondría que la función se definió en otro archivo. Si este no es el caso, no hay forma de que el compilador lo sepa: no analiza el contenido de más de un archivo a la vez. El vinculador, por otro lado, puede mirar varios archivos e intentar encontrar referencias para las funciones que no se mencionaron.

Puede preguntar por qué hay pasos separados de compilación y vinculación. Primero, probablemente sea más fácil implementar las cosas de esa manera. El compilador hace lo suyo, y el enlazador hace lo suyo: al mantener las funciones separadas, se reduce la complejidad del programa. Otra ventaja (más obvia) es que esto permite la creación de programas grandes sin tener que rehacer el paso de compilación cada vez que se cambia un archivo. En cambio, usando la llamada "compilación condicional", es necesario compilar solo aquellos archivos fuente que han cambiado; Por lo demás, los archivos de objeto son suficiente entrada para el vinculador. Finalmente, esto simplifica la implementación de bibliotecas de código precompilado: solo cree archivos de objetos y vincúlelos como cualquier otro archivo de objetos.

Para obtener todos los beneficios de la compilación de condiciones, probablemente sea más fácil obtener un programa que lo ayude que intentar recordar qué archivos ha cambiado desde la última vez que compiló. (Por supuesto, puede volver a compilar cada archivo que tenga una marca de tiempo mayor que la marca de tiempo del archivo de objeto correspondiente). Si está trabajando con un entorno de desarrollo integrado (IDE), es posible que ya se encargue de esto. Si usa herramientas de línea de comandos, hay una ingeniosa utilidad llamada make que viene con la mayoría de las distribuciones * nix. Junto con la compilación condicional, tiene varias otras características agradables para la programación, como permitir diferentes compilaciones de su programa, por ejemplo, si tiene una versión que produce una salida detallada para la depuración.

Conocer la diferencia entre la fase de compilación y la fase de enlace puede facilitar la búsqueda de errores. Los errores del compilador suelen ser de naturaleza sintáctica: falta un punto y coma, un paréntesis adicional. Los errores de vinculación generalmente tienen que ver con la falta o múltiples definiciones Si recibe un error de que una función o variable se define varias veces desde el vinculador, es una buena indicación de que el error es que dos de sus archivos de código fuente tienen la misma función o variable.

neuronet
fuente
1
Lo que no entiendo es que si el preprocesador maneja cosas como #incluye crear un súper archivo, ¿entonces no hay nada que vincular después de eso?
binarysmacker
@binarysmacer Vea si lo que escribí a continuación tiene sentido para usted. Traté de describir el problema de adentro hacia afuera.
Vista elíptica el
3
@binarysmacker Es demasiado tarde para comentar sobre esto, pero otros pueden encontrar esto útil. youtu.be/D0TazQIkc8Q Básicamente, usted incluye archivos de encabezado y estos archivos de encabezado generalmente contienen solo las declaraciones de variables / funciones y no hay definiciones, las definiciones pueden estar presentes en un archivo fuente separado. Por lo tanto, el preprocesador solo incluye declaraciones y no definiciones aquí es donde El enlazador ayuda. Usted vincula el archivo fuente que usa la variable / función con el archivo fuente que los define.
Karan Joisher
24

En el frente estándar:

  • una unidad de traducción es la combinación de archivos fuente, encabezados incluidos y archivos fuente menos las líneas fuente omitidas por la directiva de preprocesador de inclusión condicional.

  • El estándar define 9 fases en la traducción. Los primeros cuatro corresponden al preprocesamiento, los siguientes tres son la compilación, el siguiente es la creación de instancias de plantillas (produciendo unidades de creación de instancias ) y el último es la vinculación.

En la práctica, la octava fase (la creación de instancias de plantillas) a menudo se realiza durante el proceso de compilación, pero algunos compiladores la retrasan a la fase de vinculación y otros la distribuyen en las dos.

Un programador
fuente
14
¿Podría enumerar las 9 fases? Creo que sería una buena adición a la respuesta. :)
jalf
@jalf: Relacionado: stackoverflow.com/questions/1476892/… .
sbi
@jalf, solo agregue la instanciación de plantilla justo antes de la última fase en la respuesta señalada por @sbi. IIRC hay diferencias sutiles en la redacción precisa en el manejo de caracteres anchos, pero no creo que aparezcan en las etiquetas del diagrama.
Programador
2
@sbi, sí, pero se supone que esta es la pregunta frecuente, ¿no? Entonces, ¿no debería estar disponible esta información aquí ? ;)
jalf
3
@AProgrammmer: simplemente enumerarlos por nombre sería útil. Entonces las personas saben qué buscar si quieren más detalles. De todos modos, hice +1 en su respuesta en cualquier caso :)
jalf
14

Lo flaco es que una CPU carga datos de direcciones de memoria, almacena datos en direcciones de memoria y ejecuta instrucciones secuencialmente fuera de las direcciones de memoria, con algunos saltos condicionales en la secuencia de instrucciones procesadas. Cada una de estas tres categorías de instrucciones implica calcular una dirección a una celda de memoria para ser utilizada en la instrucción de la máquina. Debido a que las instrucciones de la máquina son de una longitud variable dependiendo de la instrucción particular involucrada, y debido a que encadenamos una longitud variable de ellas juntas a medida que construimos nuestro código de máquina, hay un proceso de dos pasos involucrados en el cálculo y la construcción de cualquier dirección.

Primero, diseñamos la asignación de memoria lo mejor que podemos antes de saber qué ocurre exactamente en cada celda. Descubrimos los bytes, o palabras, o lo que sea que forme las instrucciones y literales y cualquier dato. Simplemente comenzamos a asignar memoria y a construir los valores que crearán el programa a medida que avanzamos, y anotamos cualquier lugar que necesitemos para regresar y corregir una dirección. En ese lugar, colocamos un maniquí para rellenar la ubicación y poder seguir calculando el tamaño de la memoria. Por ejemplo, nuestro primer código de máquina podría tomar una celda. El siguiente código de máquina puede tomar 3 celdas, involucrando una celda de código de máquina y dos celdas de dirección. Ahora nuestro puntero de dirección es 4. Sabemos lo que pasa en la celda de la máquina, que es el código operativo, pero tenemos que esperar para calcular qué pasa en las celdas de dirección hasta que sepamos dónde se ubicarán esos datos, es decir

Si hubiera un solo archivo fuente, un compilador podría producir teóricamente un código de máquina totalmente ejecutable sin un vinculador. En un proceso de dos pasos, podría calcular todas las direcciones reales de todas las celdas de datos a las que hace referencia cualquier carga de máquina o instrucciones de almacenamiento. Y podría calcular todas las direcciones absolutas a las que hacen referencia las instrucciones de salto absoluto. Así es como funcionan los compiladores más simples, como el de Forth, sin enlazador.

Un enlazador es algo que permite compilar bloques de código por separado. Esto puede acelerar el proceso general de creación de código y permite cierta flexibilidad con la forma en que los bloques se utilizan más tarde, en otras palabras, se pueden reubicar en la memoria, por ejemplo, agregando 1000 a cada dirección para desplazar el bloque por 1000 celdas de direcciones.

Entonces, lo que genera el compilador es un código de máquina aproximado que aún no está completamente construido, pero se presenta para que sepamos el tamaño de todo, en otras palabras, para que podamos comenzar a calcular dónde se ubicarán todas las direcciones absolutas. el compilador también genera una lista de símbolos que son pares de nombre / dirección. Los símbolos relacionan un desplazamiento de memoria en el código de máquina en el módulo con un nombre. El desplazamiento es la distancia absoluta a la ubicación de memoria del símbolo en el módulo.

Ahí es donde llegamos al enlazador. El enlazador primero golpea todos estos bloques de código de máquina de extremo a extremo y anota dónde comienza cada uno. Luego, calcula las direcciones que se van a corregir sumando el desplazamiento relativo dentro de un módulo y la posición absoluta del módulo en el diseño más grande.

Obviamente, he simplificado demasiado esto para que pueda intentar comprenderlo, y deliberadamente no he usado la jerga de los archivos de objetos, tablas de símbolos, etc., lo cual para mí es parte de la confusión.

mi nombre de usuario fue secuestrado aquí
fuente
13

GCC compila un programa C / C ++ en ejecutable en 4 pasos.

Por ejemplo, gcc -o hello hello.cse lleva a cabo de la siguiente manera:

1. Preprocesamiento

Preprocesamiento a través del preprocesador GNU C ( cpp.exe), que incluye los encabezados ( #include) y expande las macros ( #define).

cpp hello.c > hello.i

El archivo intermedio resultante "hello.i" contiene el código fuente expandido.

2. Compilación

El compilador compila el código fuente preprocesado en código ensamblador para un procesador específico.

gcc -S hello.i

La opción -S especifica que se produzca código de ensamblaje, en lugar de código objeto. El archivo de ensamblaje resultante es "hello.s".

3. Asamblea

El ensamblador ( as.exe) convierte el código de ensamblaje en código de máquina en el archivo de objeto "hello.o".

as -o hello.o hello.s

4. Linker

Finalmente, el enlazador ( ld.exe) vincula el código del objeto con el código de la biblioteca para producir un archivo ejecutable "hola".

    ld -o hello hello.o ... bibliotecas ...
kaps
fuente
9

Mire la URL: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
El proceso completo de compilación de C ++ se introduce claramente en esta URL.

Charles Wang
fuente
2
Gracias por compartir eso, es tan simple y directo de entender.
Marcar el
Bien, recurso, ¿puedes poner alguna explicación básica del proceso aquí, la respuesta está marcada por el algoritmo como baja calidad b / c es corta y solo la url.
JasonB
Un bonito tutorial corto que encontré: calleerlandsson.com/the-four-stages-of-compiling-ac-program
Guy Avraham