Müssen Compiler-Writer tatsächlich Maschinencode 'verstehen'? [geschlossen]

10

Könnte eine seltsame Frage sein.

Ein Typ, der einen C ++ - Compiler schreibt (oder eine andere Sprache als VM): Muss er in der Lage sein, rohe Maschinensprache zu lesen / schreiben? Wie funktioniert das?

BEARBEITEN: Ich beziehe mich speziell auf Compiler, die mit Maschinencode kompiliert werden, nicht mit einer anderen Programmiersprache.

Aviv Cohn
quelle
1
Nein. Sie müssen es nicht einmal wissen, Sie können Ihre ISA- Zielspezifikation
SK-Logik
1
Coffescript wird zu Javascript kompiliert .
Kartik
@Kartik Enthält der CoffeeScript-Compiler, der mit Javascript kompiliert wird, auch einen Javascript-Compiler, der mit Javascript kompiliert wird? Oder wird es nur mit Javascript-Quellcode kompiliert und nicht mehr?
Aviv Cohn
Der Coffeescript-Compiler konvertiert einfach Cofeescript in Javascript. Javascript wird nicht kompiliert, sondern vom Browser verarbeitet. Ich wollte sagen, dass man einen Compiler schreiben kann, der eine Sprache in eine andere kompiliert, dafür muss man keine Maschinensprache kennen. Ein weiteres Beispiel ist der 'SPL'-Compiler, der Shakespeare-Spiele in C ++ kompiliert. shakespearelang.sourceforge.net
Kartik

Antworten:

15

Nein überhaupt nicht. Es ist durchaus möglich (und oft sogar vorzuziehen), dass Ihr Compiler stattdessen Assembly-Code ausgibt. Der Assembler kümmert sich dann um die Erstellung des eigentlichen Maschinencodes.

Übrigens ist Ihre Unterscheidung zwischen Nicht-VM-Implementierung und VM-Implementierung nicht sinnvoll.

  • Für den Anfang sind die Verwendung einer VM oder die Vorkompilierung von Maschinencode nur verschiedene Möglichkeiten, eine Sprache zu implementieren. In den meisten Fällen kann eine Sprache mit beiden Strategien implementiert werden. Ich musste tatsächlich einmal einen C ++ - Interpreter verwenden .

  • Außerdem haben viele VMs wie die JVM einen binären Maschinencode und einen Assembler, genau wie eine gewöhnliche Architektur.

Das LLVM (das von den Clang-Compilern verwendet wird) verdient hier besondere Erwähnung: Es definiert eine VM, für die Anweisungen entweder als Bytecode, Textassemblierung oder als Datenstruktur dargestellt werden können, was die Ausgabe von einem Compiler sehr einfach macht. Obwohl es für das Debuggen nützlich wäre (und um zu verstehen, was Sie tun), müssten Sie nicht einmal die Assemblersprache kennen, sondern nur die LLVM-API.

Das Schöne an der LLVM ist, dass ihre VM nur eine Abstraktion ist und dass der Bytecode normalerweise nicht interpretiert wird, sondern stattdessen transparent JITted. Es ist also durchaus möglich, eine Sprache zu schreiben, die effektiv kompiliert wird, ohne jemals etwas über den Befehlssatz Ihrer CPU wissen zu müssen.

amon
quelle
Eine weitere nette Eigenschaft von LLVM ist, dass man die Ziel-ISA nicht genau verstehen muss, um ein effizientes Backend zu implementieren. Es ist ziemlich deklarativ, so dass man eine ISA-Spezifikation fast kopieren und in .td-Dateien einfügen kann, ohne zu versuchen, sie zu verstehen.
SK-Logik
Danke für die Antwort. Frage: Ich verstehe aus Ihrer Antwort und den Antworten anderer, dass der Compiler-Writer den Maschinencode nicht verstehen muss. Er kann ein anderes Tool verwenden, das die eigentliche Konvertierung in Maschinencode für ihn vornimmt. Der Typ, der dieses Tool geschrieben hat, musste jedoch die Maschinensprache verstehen, oder? Der Typ, der die Software geschrieben hat, die die eigentliche Konvertierung von einer Sprache in Maschinencode durchführt, muss die Maschinensprache tatsächlich verstehen, oder?
Aviv Cohn
5
@Prog ja. Wenn Sie eine Abstraktionsebene erstellen, müssen Sie nur die Ebene unter Ihnen verstehen. Ein grundlegendes Verständnis aller Ebenen ist zwar hilfreich, aber nicht unbedingt erforderlich. Sie müssen die Quantenphysik nicht verstehen, um einen Transistor zu verwenden. Sie müssen das Chipdesign nicht verstehen, um eine CPU verwenden zu können. Sie müssen den Maschinencode nicht kennen, um eine Baugruppe schreiben zu können. Sie müssen den Befehlssatz der Plattform nicht kennen, wenn Sie eine VM usw. verwenden. Aber jemand musste diese Abstraktionsebenen unter Ihrer erstellen.
Amon
1
@ SK-Logik Nicht wahr (zumindest wenn Sie guten Code wollen) von dem, was ich gehört habe. Die Leute, die das Aarch64-Backend für llvm implementiert haben, hatten einige Herausforderungen (Umzüge für einen, Ladungsspeichermuster schrecklich, ..). Und das ignoriert den größten Elefanten im Raum: Das Speichermodell der ISA und das Speichermodell der Sprache, an der Sie interessiert sind. Sie können an einem Compiler arbeiten, aber Sie können nicht an einem Backend arbeiten, ohne die Architektur zu verstehen.
Voo
1
Ich würde hinzufügen, dass es konzeptionell wirklich keinen wesentlichen Unterschied zwischen Baugruppe und Maschinencode gibt. Sie werden nicht wirklich viel davon haben, wenn Sie erst einmal wissen, wie man eine bestimmte Anweisung verwendet, wie der Opcode dieser Anweisung lautet. Wenn Sie wissen, wie man MOV verwendet, spielt es keine Rolle, ob Sie nicht wissen, dass es sich um Anweisung 27 handelt, die meiner Meinung nach der Beschreibung von @ SK-Logik ähnelt.
Whatsisname
9

Nein. Der entscheidende Punkt Ihrer Frage ist, dass die Zusammenstellung ein äußerst weit gefasster Begriff ist. Die Kompilierung kann von jeder Sprache zu jeder Sprache erfolgen. Und Assembly- / Maschinencode ist nur eine der vielen Sprachen für das Kompilierungsziel. Beispielsweise werden Java- und .NET-Sprachen wie C #, F # und VB.NET zu einer Art Zwischencode anstelle von maschinenspezifischem Code kompiliert. Es spielt keine Rolle, ob es dann auf einer VM ausgeführt wird, die Sprache ist noch kompiliert. Es gibt auch die Möglichkeit, in eine andere Sprache zu kompilieren, wie z. B. C. C ist ein sehr beliebtes Kompilierungsziel, und viele Tools tun dies. Und schließlich können Sie ein Tool oder eine Bibliothek verwenden, um die harte Arbeit der Erstellung von Maschinencode für Sie zu erledigen. Es gibt zum Beispiel LLVM, das den Aufwand für die Erstellung eines eigenständigen Compilers reduzieren kann.

Außerdem macht Ihre Bearbeitung keinen Sinn. Es ist wie zu fragen: "Muss jeder Ingenieur verstehen, wie der Motor funktioniert? Und ich frage nach Ingenieuren, die an Motoren arbeiten." Wenn Sie an einem Programm oder einer Bibliothek arbeiten, die einen Maschinencode ausgibt, müssen Sie ihn verstehen. Der Punkt ist, dass Sie so etwas nicht tun müssen, wenn Sie einen Compiler schreiben. Viele Leute haben es vor Ihnen getan, daher müssen Sie ernsthafte Gründe haben, es erneut zu tun.

Euphorisch
quelle
Und die Person, die das Tool oder die Bibliothek schreibt, die die eigentliche Konvertierung in die Maschinensprache vornimmt, muss die Maschinensprache vollständig verstehen, oder?
Aviv Cohn
3
@Prog Müssen Sie eine Programmiersprache vollständig verstehen, um darin programmieren zu können? Nein, aber Sie schreiben möglicherweise suboptimalen Code und können bestimmte Dinge nicht tun, zu denen andere möglicherweise in der Lage sind. Müssen Sie die Maschinensprache vollständig verstehen, wenn Sie einen Compiler schreiben, der in diese übersetzt. Nein, aber Ihr Compiler ist nicht optimal und kann bestimmte Dinge nicht tun.
Sumurai8
@ Sumurai8: Obwohl Sie die Maschinensprache bis zu einem gewissen Grad stückweise "verstehen" können, um einen Maschinencode-Emitter zu schreiben, der das Ganze besser versteht als Sie. Wenn Sie beispielsweise ein gutes Framework schreiben, können Sie die Definition jedes Opcodes zusammen mit seinen Kosten und Überlegungen zum Pipelining konfigurieren. Anschließend kann Ihr Framework optimierten Maschinencode schreiben, obwohl Sie keinerlei Erfahrung mit der Optimierung dieser bestimmten Maschine haben. Es tut wahrscheinlich nicht weh, diesen Maschinencode kompetent selbst programmieren zu können.
Steve Jessop
@SteveJessop Wenn Sie jeden Opcode bis zu einem Punkt verstehen, an dem Sie einer Maschine beibringen können, wie man diesen Opcode mit anderen Opcodes verkettet, um ein übergeordnetes Konzept auszudrücken, verstehen Sie die Maschinensprache vollständig. Sie sind dann einfach zu faul, um die optimale Lösung für jedes Problem da draußen zu finden ;-)
Sumurai8
@ Sumurai8: hmm, aber zumindest im Prinzip könnte ich jeden Opcode für die fünf Minuten, die ich für die Konfiguration benötige, kurz "verstehen" und ihn dann vergessen haben, bis ich den Opcode nach dem nächsten "verstehe". Das ist wahrscheinlich nicht das, was der Fragesteller mit "in der Lage sein, rohe Maschinensprache zu lesen / schreiben" meint. Natürlich gehe ich hier von einem verdammt guten Framework aus, das konfigurierbar genug ist, um alle nützlichen Informationen zu jedem Opcode des Befehlssatzes zu definieren und zu verwenden. LLVM zielt etwas darauf ab, aber laut "Voo" (in einem Kommentar unten) hat es nicht getroffen.
Steve Jessop
3

Klassischerweise besteht ein Compiler aus drei Teilen: lexikalische Analyse, Analyse und Codegenerierung. Die lexikalische Analyse unterteilt den Programmtext in Schlüsselwörter, Namen und Werte der Sprache. Beim Parsen wird dargestellt, wie die aus der lexikalischen Analyse stammenden Token in syntaktisch korrekten Anweisungen für die Sprache kombiniert werden. Die Codegenerierung verwendet die vom Parser erzeugten Datenstrukturen und übersetzt sie in Maschinencode oder eine andere Darstellung. Heutzutage kann die lexikalische Analyse und Analyse in einem einzigen Schritt kombiniert werden.

Es ist klar, dass die Person, die den Codegenerator schreibt, den Code des Zielcomputers auf einer sehr tiefen Ebene verstehen muss, einschließlich Befehlssätzen, Prozessor-Pipelines und Cache-Verhalten. Andernfalls wären die vom Compiler erstellten Programme langsam und ineffizient. Sie sind möglicherweise in der Lage, Maschinencode zu lesen und zu schreiben, wie er durch Oktal- oder Hexadezimalzahlen dargestellt wird, aber sie schreiben im Allgemeinen Funktionen zum Generieren des Maschinencodes, wobei sie intern auf Tabellen mit Maschinenanweisungen verweisen. Theoretisch wissen die Leute, die den Lexer und den Parser schreiben, möglicherweise nichts über die Generierung des Maschinencodes. In der Tat können Sie mit einigen modernen Compilern Ihre eigenen Codegenerierungsroutinen einbinden, die möglicherweise Maschinencode für eine CPU ausgeben, von der Lexer- und Parser-Autoren noch nie gehört haben.

In der Praxis wissen Compiler-Autoren bei jedem Schritt jedoch viel über verschiedene Prozessorarchitekturen, und dies hilft ihnen beim Entwerfen der Datenstrukturen, die der Codegenerierungsschritt benötigt.

Charles E. Grant
quelle
2

Vor langer Zeit habe ich einen Compiler geschrieben, der zwischen zwei verschiedenen Shell-Skripten konvertiert hat. Es kam dem Maschinencode nicht nahe.

Ein Compiler-Schreibzugriff muss seine Ausgabe verstehen , aber das ist oft kein Maschinencode.

Die meisten Programmierer werden niemals einen Compiler schreiben, der Maschinencode oder Assemblycode ausgibt, aber benutzerdefinierte Compiler können in vielen Projekten sehr nützlich sein, um andere Ausgaben zu erzeugen.

YACC ist ein solcher Compiler, der keinen Maschinencode ausgibt.

Ian
quelle
0

Sie nicht brauchen , zu beginnen mit einer detaillierten Kenntnis der Semantik des Eingangs- und Ausgangssprachen, aber Sie besser Finish mit einem exquisit detaillierte Kenntnisse von beiden, sonst wird Ihr Compiler wird unusably Buggy. Wenn Ihre Eingabe also C ++ ist und Ihre Ausgabe eine bestimmte Maschinensprache ist, müssen Sie eventuell die Semantik beider kennen.

Hier sind einige der Feinheiten beim Kompilieren von C ++ zu Maschinencode: (Ich bin mir sicher, dass ich noch mehr vergesse.)

  1. Welche Größe wird intsein? Die "richtige" Wahl ist hier eine Kunst, die sowohl auf der natürlichen Zeigergröße der Maschine, der Leistung der ALU für verschiedene Größen von Rechenoperationen als auch auf den Entscheidungen basiert, die von vorhandenen Compilern für die Maschine getroffen werden. Hat die Maschine überhaupt 64-Bit-Arithmetik? Wenn nicht, sollte das Hinzufügen von 32-Bit-Ganzzahlen in einen Befehl übersetzt werden, während das Hinzufügen von 64-Bit-Ganzzahlen in einen Funktionsaufruf übersetzt werden sollte, um das 64-Bit-Addieren durchzuführen. Verfügt der Computer über 8-Bit- und 16-Bit-Additionsoperationen oder müssen Sie solche mit 32-Bit-Operationen und Maskierung simulieren (z. B. DEC Alpha 21064)?

  2. Welche Aufrufkonvention wird von anderen Compilern, Bibliotheken und Sprachen auf dem Computer verwendet? Werden die Parameter von rechts nach links oder von links nach rechts auf den Stapel verschoben? Gehen einige Parameter in Register, während andere auf den Stapel gehen? Befinden sich Ints und Floats in verschiedenen Registerräumen? Müssen die dem Register zugewiesenen Parameter bei varargs-Aufrufen speziell behandelt werden? Welche Register werden vom Anrufer und welche vom Angerufenen gespeichert? Können Sie Leaf-Call-Optimierungen durchführen?

  3. Was macht jede Schaltanweisung der Maschine? Wenn Sie eine 64-Bit-Ganzzahl um 65 Bit verschieben möchten, was ist das Ergebnis? (Auf vielen Maschinen entspricht das Ergebnis einer Verschiebung um 1 Bit, auf anderen ist das Ergebnis "0".)

  4. Was ist die Semantik der Speicherkonsistenz der Maschine? C ++ 11 verfügt über eine sehr gut definierte Speichersemantik, die in einigen Fällen einige Optimierungen einschränkt, in anderen Fällen jedoch Optimierungen zulässt. Wenn Sie eine Sprache kompilieren, die keine genau definierte Speichersemantik aufweist (wie jede Version von C / C ++ vor C ++ 11 und viele andere zwingende Sprachen), müssen Sie die Speichersemantik im Laufe der Zeit und normalerweise erfinden Sie möchten die Speichersemantik erfinden, die am besten zu Ihrer Maschinensemantik passt.

Wanderlogik
quelle