c++ c compiler-construction linker assembly

c++ - ¿En qué compilan C y Assembler?



compiler-construction linker (12)

C normalmente compila a ensamblador, simplemente porque eso hace la vida más fácil para el pobre escritor de compiladores.

El código de ensamblaje siempre se ensambla (no "compila") al código de objeto reubicable . Puedes pensar en esto como código binario de máquina y datos binarios, pero con mucha decoración y metadatos. Las partes clave son:

  • El código y los datos aparecen en "secciones" con nombre.

  • Los archivos de objeto reubicables pueden incluir definiciones de etiquetas , que se refieren a ubicaciones dentro de las secciones.

  • Los archivos de objetos reubicables pueden incluir "agujeros" que deben llenarse con los valores de las etiquetas definidas en otra parte. El nombre oficial para tal agujero es una entrada de reubicación .

Por ejemplo, si compila y ensambla (pero no vincula) este programa

int main () { printf("Hello, world/n"); }

es probable que termine con un archivo de objeto reubicable con

  • Una sección de text que contiene el código de máquina para main

  • Una definición de etiqueta para main que apunta al comienzo de la sección de texto

  • Una rodata (datos de solo lectura) que contiene los bytes de la cadena literal "Hello, world/n"

  • Una entrada de reubicación que depende de printf y que apunta a un "agujero" en una instrucción de llamada en el medio de una sección de texto.

Si está en un sistema Unix, un archivo de objeto reubicable generalmente se denomina archivo .o, como en hello.o , y puede explorar las definiciones de etiqueta y las utiliza con una herramienta sencilla llamada nm , y puede obtener información más detallada de un archivo. una herramienta algo más complicada llamada objdump .

Enseño una clase que cubre estos temas, y hago que los estudiantes escriban un ensamblador y un enlazador, lo que lleva un par de semanas, pero cuando lo hacen, la mayoría de ellos tienen un buen manejo del código objeto reubicable. No es tan fácil.

Así que descubrí que los programas C (++) en realidad no compilan a "binario" simple (es posible que haya obtenido algunas cosas incorrectas aquí, en ese caso lo siento: D) sino a un rango de cosas (tabla de símbolos) , cosas relacionadas con el os, ...) pero ...

  • ¿El ensamblador "compila" al binario puro? Eso significa que no hay material adicional además de recursos como cadenas predefinidas, etc.

  • Si C compila a algo más que binario simple, ¿cómo puede ese pequeño cargador de arranque ensamblador simplemente copiar las instrucciones del disco duro a la memoria y ejecutarlas? Quiero decir, si el kernel del sistema operativo, que probablemente está escrito en C, se compila en algo diferente al binario simple, ¿cómo lo maneja el gestor de arranque?

editar: Sé que el ensamblador no "compila" porque solo tiene el conjunto de instrucciones de su máquina; no encontré una buena palabra para lo que el ensamblador "ensambla". Si tiene uno, déjelo aquí como comentario y lo cambiaré.


Compilan en un archivo en un formato específico (COFF para Windows, etc.), compuesto por encabezados y segmentos, algunos de los cuales tienen códigos op "binarios simples". Los ensambladores y compiladores (como C) crean el mismo tipo de salida. Algunos formatos, como los antiguos * .COM, no tenían encabezados, pero aún tenían ciertas suposiciones (como dónde se cargaría la memoria o qué tan grande podría ser).

En máquinas con Windows, el boostrapper del sistema operativo está en un sector de disco cargado por el BIOS, donde ambos son "simples". Una vez que el sistema operativo ha cargado su cargador, puede leer los archivos que tienen encabezados y segmentos.

¿Eso ayuda?


Hay diferentes fases para traducir C ++ a un ejecutable binario. La especificación del lenguaje no establece explícitamente las fases de traducción. Sin embargo, describiré las fases de traducción comunes.

Fuente C ++ a ensamblado o lenguaje intermedio

Algunos compiladores realmente traducen el código C ++ en un lenguaje ensamblador o un lenguaje intermedio. Esta no es una fase obligatoria, pero es útil para la depuración y optimización.

Código de ensamblaje a objeto

El próximo paso común es traducir el lenguaje ensamblador a un código Object. El código objeto contiene un código ensamblador con direcciones relativas y referencias abiertas a subrutinas externas (métodos o funciones). En general, el traductor pone tanta información en un archivo objeto como puede, todo lo demás no está resuelto .

Vincular código (s) de objeto

La fase de enlace combina uno o más códigos objeto, resuelve referencias y elimina subrutinas duplicadas. El resultado final es un archivo ejecutable . Este archivo contiene información para el sistema operativo y direcciones relativas .

Ejecutando archivos binarios

El sistema operativo carga el archivo ejecutable, generalmente desde un disco duro, y lo coloca en la memoria. El sistema operativo puede convertir direcciones relativas en ubicaciones físicas. El sistema operativo también puede preparar recursos (como archivos DLL y widgets de GUI) que son necesarios para el ejecutable (que pueden figurar en el archivo ejecutable).

Compilación directa a Binary Algunos compiladores, como los que se usan en Embedded Systems, tienen la capacidad de compilar desde C ++ directamente a un código binario ejecutable. Este código tendrá direcciones físicas en lugar de direcciones relativas y no requerirá cargar un sistema operativo.

Ventajas

Una de las ventajas de estas fases es que los programas de C ++ se pueden dividir en partes, compilarse individualmente y vincularse más adelante. Incluso pueden vincularse con piezas de otros desarrolladores (también conocidas como bibliotecas). Esto permite a los desarrolladores compilar solo piezas en desarrollo y vincular en piezas que ya están validadas. En general, la traducción de C ++ a objeto es la parte que consume mucho tiempo del proceso. Además, una persona no quiere esperar a que se completen todas las fases cuando hay un error en el código fuente.

Mantenga una mente abierta y siempre espere la Tercera Alternativa (Opción) .


Hay dos cosas que puedes mezclar aquí. En general, hay dos temas:

Este último puede compilar a los primeros en el proceso de reunión. Algunos formatos intermedios no se ensamblan, sino que se ejecutan en una máquina virtual. En el caso de C ++, puede compilarse en CIL, que se ensambla en un ensamblado de .NET, por lo tanto, hay un poco de confusión.

Pero, en general, C y C ++ generalmente se compilan en binario, o en otras palabras, en un formato de archivo ejecutable.


Hay un montón de respuestas arriba para que las vea, pero pensé que agregaría estos recursos que le darán una idea de lo que sucede. Básicamente, en Windows y Linux, alguien ha intentado crear el ejecutable más pequeño posible; en Linux, ELF, windows, PE.

Ambos ejecutan lo que se elimina y por qué, y usted usa ensambladores para construir archivos ELF sin usar las opciones de sí mismo que lo hacen por usted.

Espero que ayude.

Editar: también puede ver el ensamblaje de un gestor de arranque como el de Truecrypt http://www.truecrypt.org o "stage1" de grub (el bit que realmente se escribe en MDR).


Los archivos ejecutables (formato PE en Windows) no se pueden usar para arrancar la computadora porque el cargador PE no está en la memoria.

La forma en que el bootstrapping funciona es que el registro de inicio maestro en el disco contiene una burbuja de unos pocos cientos de bytes de código. El BIOS de la computadora (en la ROM de la placa base) carga este blob en la memoria y establece el puntero de la instrucción de la CPU al comienzo de este código de arranque.

El código de arranque luego carga un cargador de "segunda etapa", en Windows llamado NTLDR (sin extensión) desde el directorio raíz. Este es un código máquina sin procesar que, al igual que el cargador MBR, se carga en la memoria fría y se ejecuta.

NTLDR tiene la capacidad completa de cargar archivos PE, incluidos archivos DLL y controladores.


Para responder a la parte de ensamblaje de la pregunta, el ensamblado no se compila en binario como yo lo entiendo. Asamblea === binario. Se traduce directamente. Cada operación de ensamblaje tiene una cadena binaria que la empareja directamente. Cada operación tiene un código binario y cada variable de registro tiene una dirección binaria.

Es decir, a menos que sea Assembler! = Assembly y estoy malinterpretando tu pregunta.


Para responder a sus preguntas, tenga en cuenta que esto es subjetivo ya que hay diferentes procesadores, diferentes plataformas, diferentes ensambladores y compiladores de C, en este caso, hablaré sobre la plataforma Intel x86.

  1. Los ensambladores no compilan en binario puro, son códigos de máquina sin formato, definidos con segmentos, como datos, texto y bss, por nombrar solo algunos, esto se llama código de objeto. El Vinculador interviene y ajusta los segmentos para hacerlo ejecutable, es decir, listo para ejecutarse. A propósito, la salida predeterminada cuando compila con gcc es ''a.out'', que es una abreviatura de Assembler Output.
  2. Los cargadores de arranque tienen una directiva especial definida, en la época de DOS, era común encontrar una directiva como .Org 100h , que define el código de ensamblador como de la antigua variedad .COM antes de que se hiciera famoso .EXE. Además, no era necesario tener un ensamblador para producir un archivo .COM, usando el viejo debug.exe que venía con MSDOS, el truco para pequeños programas simples, los archivos .COM no necesitaban un enlazador y estaban listos. para ejecutar el formato binario. Aquí hay una sesión simple usando DEBUG.

1:*a 0100 2:* mov AH,07 3:* int 21 4:* cmp AL,00 5:* jnz 010c 6:* mov AH,07 7:* int 21 8:* mov AH,4C 9:* int 21 10:* 11:*r CX 12:*10 13:*n respond.com 14:*w 15:*q

Esto produce un programa .COM listo para ejecutar llamado ''responder.com'' que espera una pulsación de tecla y no la repite en la pantalla. Tenga en cuenta, el comienzo, el uso de ''a 100h'' que muestra que el puntero de Instrucción comienza en 100h, que es la característica de un .COM. Este antiguo script se usaba principalmente en archivos por lotes esperando una respuesta y no se hacía eco de ella. El script original se puede encontrar here .

Nuevamente, en el caso de los cargadores de arranque, se convierten a un formato binario, había un programa que solía venir con DOS, llamado EXE2BIN . Ese fue el trabajo de convertir el código de objeto sin procesar en un formato que se puede copiar en un disco de arranque para el arranque. Recuerde que no se ejecuta ningún enlazador con el código ensamblado, ya que el enlazador es para el entorno de tiempo de ejecución y configura el código para hacerlo ejecutable y ejecutable.

El BIOS al arrancar, espera que el código esté en el segmento: offset, 0x7c00, si mi memoria me sirve, el código (después de ser EXE2BIN), comenzará a ejecutarse, luego el gestor de arranque se reubicará más abajo en la memoria y continuará cargando emitiendo int 0x13 para leer desde el disco, encienda la puerta A20, active el DMA, cambie al modo protegido ya que el BIOS está en modo de 16 bits, luego los datos leídos del disco se cargan en la memoria, luego el gestor de arranque emite un salto lejano en el código de datos (es probable que esté escrito en C). Eso es, en esencia, cómo se inicia el sistema.

De acuerdo, el párrafo anterior parece abstracto y simple, es posible que haya olvidado algo, pero así es en pocas palabras.

Espero que esto ayude, Saludos, Tom.


Según lo entiendo, un chipset (CPU, etc.) tendrá un conjunto de registros para almacenar datos y comprenderá un conjunto de instrucciones para manipular estos registros. Las instrucciones serán cosas como ''almacenar este valor en este registro'', ''mover este valor'' o ''comparar estos dos valores''. Estas instrucciones a menudo se expresan en códigos alfabéticos abreviados por humanos (lenguaje ensamblador o ensamblador) que se asignan a los números que el chipset entiende: esos números se presentan al chip en formato binario (código máquina).

Esos códigos son el nivel más bajo al que llega el software. Ir más allá de eso se mete en la arquitectura del chip real, que es algo en lo que no me he involucrado.


Tienes muchas respuestas para leer, pero creo que puedo mantener esto conciso.

"Código binario" se refiere a los bits que se alimentan a través de los circuitos del microprocesador. El microprocesador carga cada instrucción de la memoria en secuencia, haciendo lo que digan. Diferentes familias de procesadores tienen diferentes formatos para las instrucciones: x86, ARM, PowerPC, etc. Apunta el procesador a la instrucción que desee dándole la dirección de la instrucción en la memoria, y luego se propaga alegremente por el resto del programa.

Cuando desee cargar un programa en el procesador, primero debe hacer que el código binario sea accesible en la memoria para que tenga una dirección en primer lugar. El compilador de C genera un archivo en el sistema de archivos, que debe cargarse en un nuevo espacio de direcciones virtuales. Por lo tanto, además del código binario, ese archivo debe incluir la información de que tiene código binario y cómo debe ser su espacio de direcciones.

Un gestor de arranque tiene requisitos diferentes, por lo que su formato de archivo puede ser diferente. Pero la idea es la misma: el código binario siempre es una carga útil en un formato de archivo más grande, que incluye como mínimo un control de cordura para garantizar que esté escrito en el conjunto de instrucciones correcto.

Los compiladores y ensambladores de C suelen estar configurados para producir archivos de biblioteca estáticos. Para las aplicaciones integradas, es más probable que encuentre un compilador que produzca algo así como una imagen de memoria sin procesar con instrucciones que comiencen en la dirección cero. De lo contrario, puede escribir un enlazador que convierta la salida del compilador C en cualquier otra cosa que desee.


Tomemos un programa C

Cuando ejecuta ''gcc'' o ''cl'' en el programa c, pasará por estas etapas:

  1. Preprocessor lexing (#include, #ifdef, análisis de trigrafos, codificación de traducciones, gestión de comentarios, macros ...)
  2. Análisis léxico (producción de tokens y errores léxicos).
  3. Análisis sintáctico (que produce un árbol de análisis sintáctico y errores sintácticos).
  4. Análisis semántico (producción de una tabla de símbolos, información de scoping y errores de scoping / typing).
  5. Salida en ensamblaje (u otro formato intermedio)
  6. Optimización del ensamblaje (como arriba). Probablemente en cadenas ASM todavía.
  7. Ensamblaje del ensamblado en un formato de objeto binario.
  8. Vincular el ensamblado a las bibliotecas estáticas que se necesitan, así como reubicarlo si es necesario.
  9. Salida del ejecutable final en formato elf o coff.

En la práctica, algunos de estos pasos pueden realizarse al mismo tiempo, pero este es el orden lógico.

Tenga en cuenta que hay un ''contenedor'' de formato elf o coff alrededor del binario ejecutable real.

Encontrará que un libro sobre compiladores (recomiendo el libro Dragon , el libro introductorio estándar en el campo) tendrá toda la información que necesita y más.

Como comentó Marco, la vinculación y la carga es un área grande y el libro del Dragón se detiene más o menos en la salida del binario ejecutable. Pasar de allí a ejecutar en un sistema operativo es un proceso decentemente complejo, que cubre Levine en Linkers and Loaders .

He buscado esta respuesta para que las personas modifiquen cualquier error / agreguen información.


С (++) (no administrado) realmente se compila en binario simple. Algunas cosas relacionadas con el sistema operativo: son llamadas a la función del BIOS y del sistema operativo, son diferentes para cada sistema operativo, pero siguen siendo binarias.
1. Assembler se compila en binario puro, pero, por extraño que parezca, está menos optimizado que C (++)
2. kernel OS, así como gestor de arranque, también escrito en C, por lo que no hay problemas aquí.

Java, Managed C ++ y otras cosas de .NET, se compilan en un pseudocódigo (MSIL en .NET), lo que lo convierte en un sistema operativo cruzado y multiplataforma, pero requiere la ejecución de un intérprete o un traductor local.