it-swarm.com.de

Schreiben eines Compilers in seiner eigenen Sprache

Intuitiv scheint es, dass ein Compiler für die Sprache Foo nicht selbst in Foo geschrieben werden kann. Genauer gesagt, der first - Compiler für die Sprache Foo kann nicht in Foo geschrieben werden, aber jeder nachfolgende Compiler kann für Foo geschrieben werden.

Aber ist das tatsächlich wahr? Ich habe eine sehr vage Erinnerung daran, über eine Sprache gelesen zu haben, deren erster Compiler in "sich selbst" geschrieben wurde. Ist das möglich und wenn ja wie?

190
Dónal

Dies wird als "Bootstrapping" bezeichnet. Sie müssen zunächst einen Compiler (oder Interpreter) für Ihre Sprache in einer anderen Sprache erstellen (normalerweise Java oder C). Anschließend können Sie eine neue Version des Compilers in der Sprache Foo schreiben Sie verwenden den ersten bootstrap Compiler, um den Compiler zu kompilieren, und verwenden diesen kompilierten Compiler dann, um alles andere (einschließlich zukünftiger Versionen von sich selbst) zu kompilieren.

Die meisten Sprachen werden in der Tat auf diese Weise erstellt, zum Teil, weil Sprachentwickler die Sprache, die sie erstellen, gerne verwenden, und weil ein nicht trivialer Compiler häufig als nützlicher Maßstab dafür dient, wie "vollständig" die Sprache sein kann.

Ein Beispiel dafür wäre Scala. Sein erster Compiler wurde in Pizza erstellt, einer experimentellen Sprache von Martin Odersky. Ab Version 2.0 wurde der Compiler in Scala komplett neu geschrieben. Von diesem Zeitpunkt an konnte der alte Pizza-Compiler vollständig verworfen werden, da der neue Scala Compiler verwendet werden konnte, um sich für zukünftige Iterationen zu kompilieren.

221
Daniel Spiewak

Ich erinnere mich an einen Software Engineering Radio Podcast , in dem Dick Gabriel über das Bootstrapping des ursprünglichen LISP-Interpreters sprach, indem er eine Bare-Bones-Version in LISP schrieb auf Papier und von Hand zusammensetzte in Maschinencode. Von da an wurden die restlichen LISP-Funktionen in LISP geschrieben und mit LISP interpretiert.

71
Alan

Den vorherigen Antworten eine Neugier hinzufügen.

Hier ist ein Zitat aus dem Handbuch Linux From Scratch für den Schritt, in dem der GCC-Compiler aus seiner Quelle erstellt wird. (Linux From Scratch ist eine Möglichkeit, Linux zu installieren, die sich grundlegend von der Installation einer Distribution unterscheidet, da Sie wirklich jede einzelne Binärdatei des Ziels kompilieren müssen System.)

make bootstrap

Das Bootstrap-Ziel kompiliert GCC nicht nur, sondern mehrmals. Es verwendet die in einer ersten Runde kompilierten Programme, um sich selbst ein zweites Mal und dann ein drittes Mal zu kompilieren. Anschließend werden diese zweiten und dritten Kompilierungen verglichen, um sicherzustellen, dass sie sich fehlerfrei reproduzieren können. Dies impliziert auch, dass es korrekt kompiliert wurde.

Die Verwendung des Bootstrap-Ziels ist dadurch motiviert, dass der Compiler, mit dem die Toolchain des Zielsystems erstellt wird, möglicherweise nicht dieselbe Version des Zielcompilers hat. Wenn man so vorgeht, erhält man im Zielsystem sicher einen Compiler, der sich selbst kompilieren kann.

46

Wenn Sie Ihren ersten Compiler für C schreiben, schreiben Sie ihn in einer anderen Sprache. Nun haben Sie einen Compiler für C in, sagen wir Assembler. Schließlich kommen Sie an die Stelle, an der Sie Zeichenfolgen analysieren müssen, insbesondere Escape-Sequenzen. Sie werden Code schreiben, um \n In das Zeichen mit dem Dezimalcode 10 (und \r In 13 usw.) umzuwandeln.

Nachdem dieser Compiler fertig ist, können Sie ihn in C erneut implementieren. Dieser Vorgang heißt " bootstrapping ".

Der String-Parsing-Code wird zu:

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

Wenn dies kompiliert wird, haben Sie eine Binärdatei, die '\ n' versteht. Das heißt, Sie können den Quellcode ändern:

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

Wo ist also die Information, dass '\ n' der Code für 13 ist? Es ist in der Binärdatei! Es ist wie DNA: Das Kompilieren von C-Quellcode mit dieser Binärdatei erbt diese Informationen. Wenn sich der Compiler selbst kompiliert, gibt er dieses Wissen an seine Nachkommen weiter. Ab diesem Zeitpunkt ist es nicht mehr möglich, allein anhand der Quelle zu sehen, was der Compiler tun wird.

Wenn Sie einen Virus in der Quelle eines Programms verstecken möchten, können Sie dies folgendermaßen tun: Holen Sie sich die Quelle eines Compilers, suchen Sie die Funktion, die Funktionen kompiliert, und ersetzen Sie sie durch diese:

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" ...
}

Die interessanten Teile sind A und B. A ist der Quellcode für compileFunction, einschließlich des Virus, der wahrscheinlich in irgendeiner Weise verschlüsselt ist, sodass die Suche in der resultierenden Binärdatei nicht nahelegt. Dadurch wird sichergestellt, dass beim Kompilieren in den Compiler der Virusinjektionscode beibehalten wird.

B ist dasselbe für die Funktion, die wir durch unser Virus ersetzen möchten. Zum Beispiel könnte es die Funktion "login" in der Quelldatei "login.c" sein, die wahrscheinlich vom Linux-Kernel stammt. Wir könnten es durch eine Version ersetzen, die zusätzlich zum normalen Passwort das Passwort "joshua" für den Root-Account akzeptiert.

Wenn Sie das kompilieren und als Binärdatei verbreiten, können Sie den Virus nicht anhand der Quelle finden.

Die ursprüngliche Quelle der Idee: http://cm.bell-labs.com/who/ken/trust.html

41
Aaron Digulla

Sie können keinen Compiler selbst schreiben, da Sie nichts haben, mit dem Sie Ihren Quellcode kompilieren können. Es gibt zwei Lösungsansätze.

Am ungünstigsten ist das Folgende. Sie schreiben einen minimalen Compiler in Assembler (yuck) für einen minimalen Satz der Sprache und verwenden diesen Compiler dann, um zusätzliche Funktionen der Sprache zu implementieren. Bauen Sie Ihren Weg nach oben, bis Sie einen Compiler mit allen Sprachfunktionen für sich haben. Ein schmerzhafter Prozess, der normalerweise nur durchgeführt wird, wenn Sie keine andere Wahl haben.

Der bevorzugte Ansatz ist die Verwendung eines Cross-Compilers. Sie ändern das Back-End eines vorhandenen Compilers auf einem anderen Computer, um eine Ausgabe zu erstellen, die auf dem Zielcomputer ausgeführt wird. Dann haben Sie einen Nice Full Compiler und arbeiten auf dem Zielrechner. Am beliebtesten ist die C-Sprache, da es zahlreiche Compiler gibt, deren steckbare Backends ausgetauscht werden können.

Eine wenig bekannte Tatsache ist, dass der GNU= C++ - Compiler eine Implementierung hat, die nur die C-Teilmenge verwendet. Der Grund dafür ist, dass es normalerweise einfach ist, einen C-Compiler für einen neuen Zielcomputer zu finden, der dies ermöglicht Erstellen Sie dann den vollständigen GNU C++ - Compiler daraus. Sie haben sich jetzt darauf vorbereitet, einen C++ - Compiler auf dem Zielcomputer zu haben.

18
Phil Wright

Im Allgemeinen muss ein funktionierender (wenn primativer) Schnitt des Compilers zuerst funktionieren - dann können Sie darüber nachdenken, ihn zum Selbsthosting zu machen. Dies wird in einigen Sprachen als wichtiger Meilenstein angesehen.

Soweit ich mich an "mono" erinnere, müssen sie wahrscheinlich ein paar Dinge zur Reflektion hinzufügen, damit es funktioniert: Das Mono-Team weist immer wieder darauf hin, dass einige Dinge mit Reflection.Emit Einfach nicht möglich sind. Natürlich könnte das MS-Team das Gegenteil beweisen.

Dies hat ein paar echte Vorteile: Für den Anfang ist es ein ziemlich guter Komponententest! Und Sie müssen sich nur um eine Sprache kümmern (d. H. Es ist möglich, dass ein C # -Experte nicht viel über C++ weiß; jetzt können Sie den C # -Compiler reparieren). Aber ich frage mich, ob es hier keinen professionellen Stolz gibt: Sie wollen einfach , dass es sich selbst hostet .

Nicht ganz ein Compiler, aber ich habe kürzlich an einem System gearbeitet, das sich selbst hostet. Der Codegenerator wird zum Generieren des Codegenerators verwendet. Wenn sich das Schema ändert, führe ich es einfach auf sich selbst aus: Neue Version. Wenn es einen Fehler gibt, gehe ich einfach zu einer früheren Version zurück und versuche es erneut. Sehr praktisch und sehr pflegeleicht.


Update 1

Ich habe mir gerade dieses Video von Anders bei PDC angesehen, und (ungefähr eine Stunde später) gibt er einige viel zutreffendere Gründe an - alles über den Compiler als Service. Nur für das Protokoll.

14
Marc Gravell

Hier ist ein Dump (schwierig zu durchsuchendes Thema):

Dies ist auch die Idee von PyPy und Rubinius :

(Ich denke, das könnte auch auf Forth zutreffen, aber ich weiß nichts über Forth.)

4
Gene T

GNAT, der GNU Ada-Compiler, setzt voraus, dass ein Ada-Compiler vollständig erstellt ist. Dies kann problematisch sein, wenn Sie ihn auf eine Plattform portieren, auf der keine GNAT-Binärdatei verfügbar ist.

1
David Holm

Der C # -Compiler des Mono-Projekts ist seit langer Zeit "selbst gehostet", was bedeutet, dass er in C # selbst geschrieben wurde.

Was ich weiß ist, dass der Compiler als reiner C-Code gestartet wurde, aber sobald die "grundlegenden" Funktionen von ECMA implementiert waren, begannen sie, den Compiler in C # neu zu schreiben.

Mir sind die Vorteile nicht bekannt, den Compiler in derselben Sprache zu schreiben, aber ich bin sicher, dass dies zumindest mit den Funktionen zu tun hat, die die Sprache selbst bieten kann (C unterstützt beispielsweise keine objektorientierte Programmierung). .

Weitere Informationen finden Sie hier .

1
Gustavo Rubio

Tatsächlich sind die meisten Compiler aus den oben genannten Gründen in der Sprache geschrieben, die sie kompilieren.

Der erste bootstrap Compiler wird normalerweise in C, C++ oder Assembly geschrieben.

1
Can Berk Güder

Vielleicht können Sie ein BNF schreiben, das BNF beschreibt.

0
Eugene Yokota