compiler compilador compiler-construction bootstrapping

compiler-construction - compiler - gcc compilador



Escribir un compilador en su propio idioma (12)

Aquí hay un volcado (tema difícil para buscar, en realidad):

Esta es también la idea de PyPy y Rubinius :

(Creo que esto también podría aplicarse a Forth , pero no sé nada sobre Forth).

Intuitivamente, parece que un compilador de lenguaje Foo no puede escribirse en Foo. Más específicamente, el primer compilador de lenguaje Foo no se puede escribir en Foo, pero cualquier compilador subsiguiente podría escribirse para Foo .

Pero, ¿es esto realmente cierto? Tengo un recuerdo muy vago de leer sobre un lenguaje cuyo primer compilador fue escrito en "sí mismo". ¿Es posible? y si lo es, cómo?


Cuando escribe su primer compilador para C, lo escribe en otro idioma. Ahora, tiene un compilador para C en, por ejemplo, ensamblador. Eventualmente, llegarás al lugar donde tienes que analizar cadenas, específicamente secuencias de escape. Escribirá código para convertir /n al carácter con el código decimal 10 (y /r a 13, etc.).

Después de que el compilador esté listo, comenzará a volver a implementarlo en C. Este proceso se denomina " bootstrapping ".

El código de análisis de cadenas se convertirá en:

... if (c == 92) { // backslash c = getc(); if (c == 110) { // n return 10; } else if (c == 92) { // another backslash return 92; } else { ... } } ...

Cuando esto compila, tiene un binario que comprende ''/ n''. Esto significa que puedes cambiar el código fuente:

... if (c == ''//') { c = getc(); if (c == ''n'') { return ''/n''; } else if (c == ''//') { return ''//'; } else { ... } } ...

Entonces, ¿dónde está la información que ''/ n'' es el código para 13? ¡Está en el binario! Es como el ADN: la compilación del código fuente C con este binario heredará esta información. Si el compilador se compila a sí mismo, pasará este conocimiento a su descendencia. A partir de este momento, no hay forma de ver solo desde la fuente lo que hará el compilador.

Si desea ocultar un virus en la fuente de algún programa, puede hacerlo así: Obtenga la fuente de un compilador, encuentre la función que compila las funciones y reemplácela con esta:

void compileFunction(char * name, char * filename, char * code) { if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) { code = A; } else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) { code = B; } ... code to compile the function body from the string in "code" ... }

Las partes interesantes son A y B. A es el código fuente de compileFunction incluye el virus, probablemente encriptado de alguna manera, por lo que no es obvio al buscar el binario resultante. Esto asegura que la compilación con el compilador preservará el código de inyección de virus.

B es lo mismo para la función que queremos reemplazar con nuestro virus. Por ejemplo, podría ser la función "iniciar sesión" en el archivo de origen "login.c", que probablemente proviene del kernel de Linux. Podríamos reemplazarlo con una versión que acepte la contraseña "joshua" para la cuenta raíz, además de la contraseña normal.

Si compila y distribuye como un binario, no habrá forma de encontrar el virus mirando la fuente.

La fuente original de la idea: http://cm.bell-labs.com/who/ken/trust.html


El compilador de C # del proyecto Mono ha sido "autoalimentado" durante mucho tiempo, lo que significa es que ha sido escrito en C #.

Lo que sé es que el compilador se inició como código C puro, pero una vez que se implementaron las características "básicas" de ECMA, comenzaron a reescribir el compilador en C #.

No conozco las ventajas de escribir el compilador en el mismo idioma, pero estoy seguro de que tiene que ver al menos con las características que el lenguaje puede ofrecer (C, por ejemplo, no admite la programación orientada a objetos) .

Puede encontrar más información here .


En general, es necesario que el corte del compilador trabaje primero (si es primitivo), entonces puede comenzar a pensar en hacerlo autohospedado. En realidad, esto se considera un hito importante en algunos lenguajes.

Por lo que recuerdo de "mono", es probable que necesiten agregar algunas cosas a la reflexión para que funcione: el equipo mono sigue señalando que algunas cosas simplemente no son posibles con Reflection.Emit ; por supuesto, el equipo de MS podría demostrar que están equivocados.

Esto tiene algunas ventajas reales : es una prueba de unidad bastante buena, ¡para empezar! Y usted solo tiene un idioma del que preocuparse (es decir, es posible que un experto en C # no sepa mucho de C ++, pero ahora puede arreglar el compilador de C #). Pero me pregunto si no hay una gran cantidad de orgullo profesional trabajando aquí: simplemente quieren que sea autohospedado.

No es exactamente un compilador, pero recientemente he estado trabajando en un sistema que es self hosting; el generador de código se usa para generar el generador de código ... entonces, si el esquema cambia, simplemente lo ejecuto sobre sí mismo: nueva versión. Si hay un error, simplemente vuelvo a una versión anterior y vuelvo a intentarlo. Muy conveniente y muy fácil de mantener.

Actualización 1

Acabo de ver este video de Anders en PDC, y (alrededor de una hora) ofrece algunas razones mucho más válidas, todo sobre el compilador como un servicio. Para que conste.


En la teoría del compilador, puede usar diagramas en T para describir el proceso de arranque. Por ejemplo, mira here .

En mi tesis de licenciatura, utilicé estos diagramas en T para describir el proceso de convertir y mostrar documentos al almacenar grandes cantidades de documentos electrónicos en diferentes formatos de diferentes plataformas.


En realidad, la mayoría de los compiladores están escritos en el idioma que compilan, por las razones indicadas anteriormente.

El primer compilador de arranque generalmente se escribe en C, C ++ o Ensamblaje.


Esto se llama "arranque". Primero debe compilar un compilador (o intérprete) para su idioma en otro idioma (generalmente Java o C). Una vez hecho esto, puede escribir una nueva versión del compilador en el idioma Foo. Utiliza el primer compilador de arranque para compilar el compilador y luego utiliza este compilador compilado para compilar todo lo demás (incluidas las versiones futuras de sí mismo).

La mayoría de los lenguajes se crean de esta manera, en parte porque a los diseñadores les gusta usar el lenguaje que están creando, y también porque un compilador no trivial a menudo sirve como un punto de referencia útil para saber cuán "completo" puede ser el lenguaje.

Un ejemplo de esto sería Scala. Su primer compilador fue creado en Pizza, un lenguaje experimental de Martin Odersky. A partir de la versión 2.0, el compilador fue completamente reescrito en Scala. A partir de ese momento, el antiguo compilador de Pizza podría descartarse por completo, debido a que el nuevo compilador de Scala podría usarse para compilarse para futuras iteraciones.


GNAT, el compilador de Ada GNU, requiere un compilador Ada completamente desarrollado. Esto puede ser un problema cuando lo transfiere a una plataforma donde no hay ningún binario de GNAT disponible.


No puede escribir un compilador en sí mismo porque no tiene nada con que compilar su código fuente de inicio. Hay dos enfoques para resolver esto.

El menos favorecido es el siguiente. Escribes un compilador mínimo en ensamblador (yuck) para un conjunto mínimo del lenguaje y luego utilizas ese compilador para implementar funciones adicionales del idioma. Mejora tu camino hasta que tengas un compilador con todas las características de idioma por sí mismo. Un proceso doloroso que generalmente solo se realiza cuando no tiene otra opción.

El enfoque preferido es usar un compilador cruzado. Cambia el back-end de un compilador existente en una máquina diferente para crear un resultado que se ejecuta en la máquina de destino. Luego tiene un buen compilador completo y trabajando en la máquina de destino. El más popular para esto es el lenguaje C, ya que hay muchos compiladores existentes que tienen extremos posteriores conectables que se pueden intercambiar.

Un hecho poco conocido es que el compilador GNU C ++ tiene una implementación que usa solo el subconjunto C. La razón es que generalmente es fácil encontrar un compilador de C para una nueva máquina de destino que le permite construir el compilador completo GNU C ++. Ahora tiene arranque atado a tener un compilador de C ++ en la máquina de destino.


Quizás puedas escribir un BNF describiendo BNF.


Recuerdo haber escuchado un podcast de Radio de Ingeniería de Software en el que Dick Gabriel habló sobre el arranque del intérprete original de LISP al escribir una versión esquemática en LISP en papel y ensamblarla a mano en código máquina. A partir de entonces, el resto de las características de LISP fueron escritas e interpretadas con LISP.


Agregar curiosidad a las respuestas anteriores.

Aquí hay una cita del manual Linux From Scratch , en el paso donde uno comienza a construir el compilador GCC desde su fuente. (Linux From Scratch es una forma de instalar Linux que es radicalmente diferente de la instalación de una distribución, ya que tiene que compilar realmente todos y cada uno de los binarios del sistema de destino).

make bootstrap

El objetivo ''bootstrap'' no solo compila GCC, sino que lo compila varias veces. Utiliza los programas compilados en una primera ronda para compilarse una segunda vez, y luego una tercera vez. Luego compara estas compilaciones segunda y tercera para asegurarse de que pueda reproducirse sin problemas. Esto también implica que se compiló correctamente.

Ese uso del objetivo ''bootstrap'' está motivado por el hecho de que el compilador que uno usa para construir la cadena de herramientas del sistema objetivo puede no tener la misma versión del compilador objetivo. Procediendo de esa manera, uno seguramente obtendrá, en el sistema objetivo, un compilador que puede compilarse a sí mismo.