c++ - ejecutar - ¿Cómo funciona el proceso de compilación/vinculación?
compilar y ejecutar un programa (6)
En el frente estándar:
una unidad de traducción es la combinación de archivos de origen, encabezados incluidos y archivos de origen menos las líneas de origen 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 hasta la fase de vinculación y algunos la reparten en las dos.
¿Cómo funciona el proceso de compilación y vinculación?
(Nota: se pretende que sea una entrada a las Preguntas frecuentes sobre C ++ de Stack Overflow . Si desea criticar la idea de proporcionar una FAQ en este formulario, la publicación en el meta que inició todo esto sería el lugar para hacerlo. Respuestas a esa pregunta se monitorea en la sala de chat de C ++ , donde comenzó la idea de las preguntas frecuentes en primer lugar, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea.)
Este tema se discute en CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html
Aquí está lo que el autor escribió allí:
¡Compilar no es exactamente lo mismo que crear un archivo ejecutable! En su lugar, crear un archivo ejecutable es un proceso de varias etapas dividido en dos componentes: compilación y enlace. 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 los archivos de código fuente a un ejecutable podría ser mejor referido 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. En su lugar, el compilador simplemente produce las instrucciones en lenguaje de máquina que corresponden al archivo de código fuente que fue compilado. 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 en 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 objeto. En este paso, es común que el vinculador se queje de las funciones no definidas (comúnmente, principal). Durante la compilación, si el compilador no pudiera encontrar la definición para una función en particular, simplemente asumirá 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 mira el contenido de más de un archivo a la vez. El enlazador, por otro lado, puede mirar varios archivos e intentar encontrar referencias para las funciones que no se mencionaron.
Puede preguntar por qué hay pasos de compilación y enlace separados. Primero, probablemente es más fácil implementar las cosas de esa manera. El compilador hace su trabajo, y el enlazador hace su trabajo: 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 su lugar, utilizando la llamada "compilación condicional", es necesario compilar solo los archivos de origen que han cambiado; para el resto, los archivos de objetos son una entrada suficiente para el enlazador. Finalmente, esto facilita la implementación de bibliotecas de código precompilado: simplemente cree archivos de objetos y vincúlelos como cualquier otro archivo de objetos. (El hecho de que cada archivo se compile por separado de la información contenida en otros archivos, incidentalmente, se denomina "modelo de compilación independiente".)
Para obtener todos los beneficios de la compilación de condiciones, es probable que sea más fácil obtener un programa que lo ayude que tratar de 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 está utilizando herramientas de línea de comandos, hay una utilidad ingeniosa llamada make que viene con la mayoría de las distribuciones * nix. Junto con la compilación condicional, tiene varias otras características interesantes para la programación, como permitir diferentes compilaciones de su programa, por ejemplo, si tiene una versión que produce resultados detallados 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 enlace generalmente tienen que ver con definiciones faltantes o múltiples. 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.
GCC compila un programa C / C ++ en ejecutable en 4 pasos.
Por ejemplo, un " gcc -o hello.exe hello.c
" se lleva a cabo de la siguiente manera:
1. Pre-procesamiento
Preproceso 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 hola.i
La opción -S especifica producir código de ensamblaje, en lugar de código de objeto. El archivo de ensamblaje resultante es "hola.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".
como -o hola.o hola.s
4. enlazador
Finalmente, el enlazador (ld.exe) vincula el código objeto con el código de la biblioteca para producir un archivo ejecutable "hello.exe".
ld -o hello.exe hello.o ... bibliotecas ...
La compilación de un programa de C ++ implica tres pasos:
Preprocesamiento: el preprocesador toma un archivo de código fuente de C ++ y se ocupa de las directivas
#include
s,#define
y otras preprocesadoras. La salida de este paso es un archivo C ++ "puro" sin directivas de preprocesador.Compilación: el compilador toma la salida del preprocesador y produce un archivo de objeto a partir de él.
Enlace: el enlazador 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 #include
y #define
. Es un agnóstico de la sintaxis de C ++, por lo que debe usarse con cuidado.
Funciona en un archivo fuente de C ++ a la vez al reemplazar las directivas #include
con el contenido de los archivos respectivos (que generalmente son solo declaraciones), al reemplazar las macros ( #define
) y al seleccionar diferentes porciones de texto dependiendo de #if
, #ifdef
y #ifndef
directivas.
El preprocesador trabaja en una secuencia de tokens de preprocesamiento. La sustitución de macros se define como la sustitución de tokens con otros tokens (el operador ##
permite fusionar dos tokens cuando tiene sentido).
Después de todo esto, el preprocesador produce una salida única que es una secuencia de tokens resultantes de las transformaciones descritas anteriormente. También agrega algunos marcadores especiales que le dicen al compilador de dónde proviene cada línea para que pueda usarlos para producir mensajes de error razonables.
Algunos errores pueden producirse en esta etapa con un uso inteligente de las #error
#if
y #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 de ensamblaje. Luego invoca el back-end subyacente (ensamblador en la cadena de herramientas) que ensambla ese código en un código de máquina que produce 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 nombre.
Los archivos de objetos pueden referirse a símbolos que no están definidos. Este es el caso cuando usa una declaración y no proporciona una definición para ella. Al compilador no le importa esto, y felizmente producirá el archivo de 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 recompilar todo si solo cambia un solo archivo.
Los archivos de objetos producidos se pueden colocar en archivos especiales denominados bibliotecas estáticas, para luego reutilizarlos más fácilmente.
Es en esta etapa que se informan los errores "regulares" del compilador, como los errores de sintaxis o los errores de resolución de sobrecarga fallidos.
Enlace
El enlazador es lo que produce la salida de compilación final de los archivos de objeto que el compilador produjo. Esta salida 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 archivo ejecutable.
Vincula todos los archivos de objetos reemplazando las referencias a símbolos no definidos con las direcciones correctas. Cada uno de estos símbolos se puede definir en otros archivos de objetos o en bibliotecas. Si están definidas en bibliotecas distintas de la biblioteca estándar, debe informar al vinculador sobre ellas.
En esta etapa, los errores más comunes son las definiciones faltantes o las 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. Lo último es obvio: el mismo símbolo se definió en dos archivos de objetos o bibliotecas diferentes.
Lo mínimo 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 el cálculo de una dirección a una celda de memoria que se utilizará 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 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 las direcciones.
Primero, establecemos la asignación de memoria lo mejor que podamos antes de que podamos saber qué es exactamente lo que pasa en cada celda. Descubrimos los bytes, o palabras, o lo que sea que forme las instrucciones y los literales y cualquier información. Simplemente comenzamos a asignar memoria y construimos los valores que crearán el programa a medida que avanzamos, y anotamos en cualquier lugar que necesitemos regresar y arreglar una dirección. En ese lugar, colocamos un dummy para simplemente rellenar la ubicación para que podamos continuar 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, lo que implica una celda de código de máquina y dos celdas de dirección. Ahora nuestro indicador de dirección es 4. Sabemos lo que pasa en la celda de la máquina, que es el código de operación, pero tenemos que esperar para calcular lo que va en las celdas de dirección hasta que sepamos dónde se ubicarán esos datos, es decir, cuál será la Dirección de la máquina de esos datos.
Si hubiera un solo archivo de origen, un compilador podría, en teoría, producir un código de máquina completamente ejecutable sin un vinculador. En un proceso de dos pasadas, podría calcular todas las direcciones reales de todas las celdas de datos a las que hace referencia cualquier carga de la máquina o instrucciones de almacenamiento. Y podría calcular todas las direcciones absolutas a las que hace referencia cualquier instrucción 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 del código de construcción y permite cierta flexibilidad con la forma en que los bloques se usarán más adelante, en otras palabras, se pueden reubicar en la memoria, por ejemplo, agregar 1000 a cada dirección para acelerar el bloque en 1000 celdas de direcciones.
Entonces, lo que el compilador produce es un código de máquina en bruto que aún no está completamente construido, pero que está diseñado para que sepamos el tamaño de todo, en otras palabras, podemos 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 juntos de extremo a extremo y anota dónde comienza cada uno. Luego calcula las direcciones que se fijarán 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, lo he simplificado demasiado para que pueda intentar captarlo, y deliberadamente no he usado la jerga de archivos de objetos, tablas de símbolos, etc., que para mí es parte de la confusión.
Mire la URL: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
El proceso de cumplimiento completo de C ++ se introduce claramente en esta URL.