Ein Dolmetscher arbeitet so, wie Sie es erraten haben. In einem einfachen Modell wird ein Wörterbuch mit den Variablennamen als Wörterbuchschlüssel und den Variablenwerten als Wörterbuchwert verwaltet. Wenn die Sprache das Konzept von Variablen kennt, die nur in bestimmten Kontexten sichtbar sind, verwaltet der Interpreter mehrere Wörterbücher, um die verschiedenen Kontexte widerzuspiegeln. Der Interpreter selbst ist normalerweise ein kompiliertes Programm. Informationen zur Speicherung finden Sie weiter unten.
Compiler
(Dies hängt sehr stark von der Sprache und dem Compiler ab und ist extrem vereinfacht, daher soll es nur eine Idee geben.)
Nehmen wir an, wir haben eine globale Variable int five = 5. Eine globale Variable existiert nur einmal im Programm, daher reserviert der Compiler einen Speicherbereich von 4 Bytes (int size) in einem Datenbereich. Es kann eine feste Adresse verwenden, sagen wir 1234. In die ausführbare Datei fügt der Compiler die Information ein, dass die vier Bytes ab 1234 als statischer Datenspeicher benötigt werden, beim Programmstart mit der Nummer 5 gefüllt werden sollen und optional (z Debugger-Unterstützung) die Information, dass der 1234-Ort aufgerufen wird fiveund eine Ganzzahl enthält. Wenn sich eine andere Codezeile auf die genannte Variable bezieht, fivemerkt sich der Compiler, dass sie bei 1234 platziert ist, und fügt eine Speicherlese- oder Schreibanweisung für die Adresse 1234 ein.
Wenn int six = 6es sich um eine lokale Variable innerhalb einer Funktion handelt, sollte sie für jeden derzeit aktiven Aufruf dieser Funktion einmal vorhanden sein (aufgrund von Rekursion oder Multithreading kann es mehrere geben). Jeder Funktionsaufruf stapelt also genügend Speicherplatz auf dem Stapel, um seine Variablen zu speichern (einschließlich vier Bytes für unsere sixVariable. Der Compiler entscheidet, wo die sixVariable in diesem Stapelrahmen platziert werden soll, möglicherweise 8 Byte vom Rahmenstart entfernt, und merkt sich diese relative Position. Die Anweisungen, die der Compiler für die Funktion erstellt, lauten also:
Stellen Sie den Stapelzeiger um genügend Bytes für alle lokalen Variablen der Funktion vor.
Speichern Sie die Zahl 6 (Anfangswert von six) an der Speicherstelle 8 Bytes über dem Stapelzeiger.
Wo immer sich die Funktion bezieht six, fügt der Compiler eine Lese- oder Schreibanweisung für die Speicherposition 8 Bytes über dem Stapelzeiger ein.
Wenn Sie mit der Funktion fertig sind, spulen Sie den Stapelzeiger auf seinen alten Wert zurück.
Noch einmal, das ist nur ein sehr vereinfachtes Modell, das nicht alle Variablentypen abdeckt, aber vielleicht hilft es, ein Verständnis zu erlangen ...
Für jemanden mit meinem Kenntnisstand leicht zu verstehen. Vielen Dank! Sie haben wirklich etwas Licht ins Dunkel gebracht!
Baranskistad
Es wird noch verrückter, wenn der Compiler nicht das ganze Ding in eine monolithische Binärdatei kompiliert, sondern in kleine Kompilierungseinheiten (z. B. eine pro Quelldatei). Dann müssen alle externen Dinge (wie Globals) ihre Adressen später durch ein separates Tool namens Linker aufgelöst werden (das all diese kleinen "Objekt" -Dateien zu einer größeren Binärdatei kombiniert).
Kat
Wenn man mit älteren Versionen von C vertraut ist, macht die Art der Funktionsweise lokaler Variablen deutlich, warum C früher die Deklaration aller lokalen Variablen am oberen Rand der Funktion erforderte. Später entschieden wir, dass es machbar genug war, zuerst die gesamte Funktion zu analysieren, damit Sie endlich Variablendeklarationen haben konnten, wo immer Sie wollten.
Kat
Schließlich ist es vielleicht auch relevant, dass Sie nichts daran hindert, ein Wörterbuch für lokale Variablen mit einem Compiler zu verwenden. Dies würde die dynamische Eingabe sicherlich erheblich vereinfachen (dynamische Typen können nicht vollständig auf dem Stapel gespeichert werden, da sie zur Kompilierungszeit keine bekannte Größe haben). Einfach Wörterbücher sind langsam. Übrigens, warum die meisten kompilierten Sprachen statische Typisierung verwenden.
Kat
10
Das hängt von der Implementierung ab.
Beispielsweise kann ein C-Compiler während der Kompilierung eine Symboltabelle verwalten. Dies ist eine umfangreiche Datenstruktur, die das Verschieben und Löschen von Bereichen ermöglicht, da jede Klammer zum Öffnen von zusammengesetzten Anweisungen {möglicherweise einen neuen Bereich für neue lokale Variablen einführt. Zusätzlich zur Behandlung der kommenden und gehenden Bereiche werden die deklarierten Variablen aufgezeichnet und für jeden die Namen und ihre Typen enthalten.
Diese Symboltabellendatenstruktur unterstützt auch das Nachschlagen der Informationen einer Variablen nach Namen, z. B. nach ihrem Bezeichner, und der Compiler tut dies, wenn er die deklarierten Variableninformationen an Rohbezeichner bindet, die er in der Analyse sieht. Dies ist also ziemlich früh während der Kompilierung.
Irgendwann weist der Compiler den Variablen Positionen zu. Möglicherweise werden Standortzuweisungen in derselben Symboltabellendatenstruktur aufgezeichnet. Der Compiler kann die Standortzuweisung direkt während des Parsens durchführen, kann jedoch wahrscheinlich bessere Arbeit leisten, wenn er nicht nur nach dem Parsen, sondern auch nach der allgemeinen Optimierung wartet.
Zu einem bestimmten Zeitpunkt weist der Compiler für lokale Variablen entweder einen Stapelspeicherort oder ein CPU-Register zu (dies kann komplexer sein, da die Variable tatsächlich mehrere Speicherorte haben kann, z. B. einen Stapelspeicherort für einige Teile des generierten Codes und eine CPU für andere Abschnitte registrieren).
Schließlich generiert der Compiler den tatsächlichen Code: Maschinenanweisungen, die die Werte der Variablen direkt anhand ihrer CPU-Register oder der zugewiesenen Stapelposition referenzieren, je nach Bedarf, um den zu kompilierenden Code auszuführen. Jede Zeile des Quellcodes wird zu einer eigenen Reihe von Maschinencode-Anweisungen kompiliert, sodass die generierten Anweisungen nicht nur die Operationen (Addieren, Subtrahieren), sondern auch die Positionen der Variablen codieren, auf die verwiesen wird.
Der endgültige Objektcode, der aus dem Compiler kommt, hat keine Variablennamen und -typen mehr. Es gibt nur Speicherorte, Stapelspeicherorte oder CPU-Register. Ferner gibt es keine Tabelle von Orten, sondern diese Orte werden von jedem Maschinenbefehl verwendet, der den Ort kennt, an dem der Wert der Variablen gespeichert ist. Kein Nachschlagen von Bezeichnern im Laufzeitcode, jedes Bit des generierten Codes kennt einfach die auszuführende Operation und die zu verwendenden Speicherorte.
Wenn das Debuggen während der Kompilierung aktiviert ist, gibt der Compiler eine Form der Symboltabelle aus, sodass Debugger beispielsweise die Namen der Variablen an den verschiedenen Stapelpositionen kennen.
Einige andere Sprachen müssen zur Laufzeit dynamisch nach Bezeichnern suchen, sodass sie möglicherweise auch eine Art Symboltabelle zur Unterstützung dieser Anforderungen bereitstellen.
Dolmetscher haben eine Vielzahl von Optionen. Sie behalten möglicherweise eine symboltabellenähnliche Datenstruktur zur Verwendung während der Ausführung bei (zusätzlich zur Verwendung während des Parsens). Anstatt jedoch einen Stapelspeicherort zuzuweisen / zu verfolgen, speichern Sie einfach den Wert für die Variable, der dem Eintrag der Variablen in der Symboltabelle zugeordnet ist Datenstruktur.
Eine Symboltabelle wird möglicherweise eher im Heap als auf dem Stapel gespeichert (obwohl die Verwendung des Stapels für Bereiche und Variablen durchaus möglich ist, und außerdem kann sie einen Stapel im Heap imitieren, um den cachefreundlichen Vorteil des Packens der Werte der Variablen in der Nähe jedes Stapels zu erzielen other), daher verwendet ein Interpreter wahrscheinlich Heapspeicher zum Speichern der Variablenwerte, während ein Compiler Stapelpositionen verwendet. Im Allgemeinen hat der Interpreter auch nicht die Freiheit, CPU-Register als Speicher für Variablenwerte zu verwenden, da die CPU-Register ansonsten damit beschäftigt sind, die Codezeilen des Interpreters selbst auszuführen ...
Der beste Weg, um zu verstehen, in was Ihr Code kompiliert wird, besteht darin , Ihren Code in Assembly zu kompilieren . Der Assemblycode ist den Prozessoranweisungen, die ausgeführt werden, am nächsten.
Es scheint mir, dass das OP nicht nach dem kompilierten Code fragt, sondern was während der Kompilierung passiert, wenn der Compiler den Code analysiert.
Das hängt von der Implementierung ab.
Beispielsweise kann ein C-Compiler während der Kompilierung eine Symboltabelle verwalten. Dies ist eine umfangreiche Datenstruktur, die das Verschieben und Löschen von Bereichen ermöglicht, da jede Klammer zum Öffnen von zusammengesetzten Anweisungen
{
möglicherweise einen neuen Bereich für neue lokale Variablen einführt. Zusätzlich zur Behandlung der kommenden und gehenden Bereiche werden die deklarierten Variablen aufgezeichnet und für jeden die Namen und ihre Typen enthalten.Diese Symboltabellendatenstruktur unterstützt auch das Nachschlagen der Informationen einer Variablen nach Namen, z. B. nach ihrem Bezeichner, und der Compiler tut dies, wenn er die deklarierten Variableninformationen an Rohbezeichner bindet, die er in der Analyse sieht. Dies ist also ziemlich früh während der Kompilierung.
Irgendwann weist der Compiler den Variablen Positionen zu. Möglicherweise werden Standortzuweisungen in derselben Symboltabellendatenstruktur aufgezeichnet. Der Compiler kann die Standortzuweisung direkt während des Parsens durchführen, kann jedoch wahrscheinlich bessere Arbeit leisten, wenn er nicht nur nach dem Parsen, sondern auch nach der allgemeinen Optimierung wartet.
Zu einem bestimmten Zeitpunkt weist der Compiler für lokale Variablen entweder einen Stapelspeicherort oder ein CPU-Register zu (dies kann komplexer sein, da die Variable tatsächlich mehrere Speicherorte haben kann, z. B. einen Stapelspeicherort für einige Teile des generierten Codes und eine CPU für andere Abschnitte registrieren).
Schließlich generiert der Compiler den tatsächlichen Code: Maschinenanweisungen, die die Werte der Variablen direkt anhand ihrer CPU-Register oder der zugewiesenen Stapelposition referenzieren, je nach Bedarf, um den zu kompilierenden Code auszuführen. Jede Zeile des Quellcodes wird zu einer eigenen Reihe von Maschinencode-Anweisungen kompiliert, sodass die generierten Anweisungen nicht nur die Operationen (Addieren, Subtrahieren), sondern auch die Positionen der Variablen codieren, auf die verwiesen wird.
Der endgültige Objektcode, der aus dem Compiler kommt, hat keine Variablennamen und -typen mehr. Es gibt nur Speicherorte, Stapelspeicherorte oder CPU-Register. Ferner gibt es keine Tabelle von Orten, sondern diese Orte werden von jedem Maschinenbefehl verwendet, der den Ort kennt, an dem der Wert der Variablen gespeichert ist. Kein Nachschlagen von Bezeichnern im Laufzeitcode, jedes Bit des generierten Codes kennt einfach die auszuführende Operation und die zu verwendenden Speicherorte.
Wenn das Debuggen während der Kompilierung aktiviert ist, gibt der Compiler eine Form der Symboltabelle aus, sodass Debugger beispielsweise die Namen der Variablen an den verschiedenen Stapelpositionen kennen.
Einige andere Sprachen müssen zur Laufzeit dynamisch nach Bezeichnern suchen, sodass sie möglicherweise auch eine Art Symboltabelle zur Unterstützung dieser Anforderungen bereitstellen.
Dolmetscher haben eine Vielzahl von Optionen. Sie behalten möglicherweise eine symboltabellenähnliche Datenstruktur zur Verwendung während der Ausführung bei (zusätzlich zur Verwendung während des Parsens). Anstatt jedoch einen Stapelspeicherort zuzuweisen / zu verfolgen, speichern Sie einfach den Wert für die Variable, der dem Eintrag der Variablen in der Symboltabelle zugeordnet ist Datenstruktur.
Eine Symboltabelle wird möglicherweise eher im Heap als auf dem Stapel gespeichert (obwohl die Verwendung des Stapels für Bereiche und Variablen durchaus möglich ist, und außerdem kann sie einen Stapel im Heap imitieren, um den cachefreundlichen Vorteil des Packens der Werte der Variablen in der Nähe jedes Stapels zu erzielen other), daher verwendet ein Interpreter wahrscheinlich Heapspeicher zum Speichern der Variablenwerte, während ein Compiler Stapelpositionen verwendet. Im Allgemeinen hat der Interpreter auch nicht die Freiheit, CPU-Register als Speicher für Variablenwerte zu verwenden, da die CPU-Register ansonsten damit beschäftigt sind, die Codezeilen des Interpreters selbst auszuführen ...
quelle
Der beste Weg, um zu verstehen, in was Ihr Code kompiliert wird, besteht darin , Ihren Code in Assembly zu kompilieren . Der Assemblycode ist den Prozessoranweisungen, die ausgeführt werden, am nächsten.
quelle