Escribir un compilador en su propio idioma.

Resuelto Dónal asked hace 16 años • 14 respuestas

Intuitivamente, parecería que un compilador de lenguaje Foono puede escribirse en Foo. Más específicamente, el primer compilador del lenguaje Foono se puede escribir en Foo, pero cualquier compilador posterior podría escribirse en Foo.

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

Dónal avatar Oct 11 '08 08:10 Dónal
Aceptado

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

De hecho, la mayoría de los lenguajes se crean de esta manera, en parte porque a los diseñadores de lenguajes 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 determinar qué tan "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 se reescribió completamente en Scala. A partir de ese momento, el antiguo compilador Pizza podría descartarse por completo, debido a que el nuevo compilador Scala podría usarse para compilarse a sí mismo para futuras iteraciones.

Daniel Spiewak avatar Oct 11 '2008 01:10 Daniel Spiewak

Recuerdo haber escuchado un podcast de Software Engineering Radio en el que Dick Gabriel hablaba sobre cómo iniciar el intérprete LISP original escribiendo una versión básica en LISP en papel y ensamblándolo a mano en código de máquina. A partir de entonces, el resto de las funciones de LISP se escribieron e interpretaron con LISP.

Alan avatar Oct 13 '2008 18:10 Alan

Cuando escribes tu primer compilador para C, lo escribes en algún otro lenguaje. Ahora tiene un compilador para C en, digamos, ensamblador. Finalmente, llegarás al lugar donde tendrás que analizar cadenas, específicamente secuencias de escape. Escribirá código para convertir \nal carácter con el código decimal 10 (y \ra 13, etc.).

Una vez que el compilador esté listo, comenzará a reimplementarlo en C. Este proceso se llama " bootstrapping ".

El código de análisis de cadenas será:

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

Cuando esto se compila, tiene un binario que entiende '\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 de que '\n' es el código para 13? ¡Está en binario! Es como el ADN: al compilar el código fuente C con este binario se heredará esta información. Si el compilador se compila a sí mismo, transmitirá este conocimiento a su descendencia. A partir de este momento, no hay forma de ver únicamente desde la fuente qué hará el compilador.

Si desea ocultar un virus en el código fuente de algún programa, puede hacerlo así: obtenga el código fuente de un compilador, busque la función que compila 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 para compileFunctionincluir el virus, probablemente cifrado de alguna manera, por lo que no es obvio al buscar en el binario resultante. Esto asegura que la compilación en el compilador consigo mismo 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 fuente "login.c", que probablemente sea 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 lo compila y lo difunde como binario, no habrá forma de encontrar el virus mirando la fuente.

La fuente original de la idea: https://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/

Aaron Digulla avatar Jan 28 '2009 09:01 Aaron Digulla

Añadiendo una curiosidad a las respuestas anteriores.

Aquí hay una cita del manual Linux From Scratch , en el paso en el que se comienza a construir el compilador GCC desde su fuente. (Linux From Scratch es una forma de instalar Linux que es radicalmente diferente a instalar una distribución, en el sentido de que tienes que compilar realmente todos los archivos binarios del sistema de destino).

make bootstrap

El objetivo 'bootstrap' no sólo 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 se utiliza para construir la cadena de herramientas del sistema de destino puede no tener la misma versión del compilador de destino. Procediendo de esta manera es seguro obtener, en el sistema de destino, un compilador que pueda compilarse a sí mismo.

Federico A. Ramponi avatar Oct 11 '2008 07:10 Federico A. Ramponi