
Ich habe einige Emulatoren gesehen, die behaupten, dass sie ausgeführt werden, und obwohl dies der Fall ist, zeigt ihr Quellcode, dass sie nicht jede 1 und 0 direkt analysieren, um eine Anweisung zu bestimmen.
Meine Frage ist nun: Wenn der Emulator die exakt gleichen Operationscodes emulieren muss wie die echte CPU, wäre es dann nicht erforderlich, das richtige binäre Operationscodeformat eines Spiels zu analysieren, um die CPU legitim (oder überhaupt) zu emulieren?
Beispielsweise speichere ich in der Spieldatei eine Anweisung, ein Byte, das wie folgt gekennzeichnet ist:
0000 1111
Mein Programm muss überprüfen, ob diese Anweisung tatsächlich Bedeutung hat (z. B. „füge eins zum A-Register hinzu“). Aber müsste es dazu nicht jede Null und Eins in der Textdatei überprüfen?
Dann würden die Emulatoren ganze Bytes analysieren, aber ganze Bytes bestehen wiederum aus acht Bits, und schwankende Muster ändern die Operationsausgabe.
Beispielsweise könnte 0000 1111 bedeuten, dass man zu A eins addiert, 0000 1110 hingegen könnte bedeuten, dass man A zu A addiert.
Antwort1
Exposition – Versuch, die Frage direkt zu beantworten
Wenn Sie den Quellcode eines Emulators lesen und dieser bestimmte Teile einer binären (ausführbaren) Datei nicht liest und den Code trotzdem originalgetreu ausführt, gibt es drei mögliche Ergebnisse:
- Du bistfalschin der Annahme, dass der Emulator nicht jedes Bit der Datei liest, und estut in der Tat, und Sie liegen einfach falsch.
- Du bistrichtig, und der Emulator liest nicht jedes einzelne Bit, weil er bestimmte Fakten über das Verhalten des emulierten Programms annehmen kann, sodass er nicht jedes einzelne Bit lesen muss, um zu wissen, was er tun muss (vielleicht weil er erwartet, dass eine bestimmte Spiel-Engine ausgeführt wird oder ein bestimmter Typ von Grafik-API oder ein bestimmter Typ von Sound-API usw.).
- Du bistrichtig, und der Emulator liest nicht jedes einzelne Bit, weil es bestimmte Teile der ausführbaren Datei gibt, die einfach nicht erforderlich sind, um das Programm korrekt auszuführen. Dabei kann es sich um veralteten „Müll“ oder Metadaten oder irgendetwas anderes handeln, das einfach nur zusätzlicher Ballast ist, der eigentlich keine Programmfunktionalität darstellt.
- Du bistrichtig, und der Emulator liest nicht jedes einzelne Bit, weil der Emulator bestimmte Operationen im Code in Operationen höherer Ebene übersetzt und die prozessor-/hardwarespezifischen Anweisungen auf niedriger Ebene vollständig umgeht. Wenn Sie beispielsweise aufgefordert werden, genau nachzuahmen, was eine Person in einer Videoaufzeichnung tut, in der diese Person eine komplizierte Operation durchführt, und diese Person sagt: „Bohren Sie jetzt ein Loch in die Seite der Schachtel“, wären Sie ziemlich versucht, das Video nicht mehr anzusehen und Ihre vorhandenen Erfahrungen mit dem Bohren von Löchern in Dinge zu nutzen, anstatt den wörtlichen Bewegungen der Person im Video zu folgen (vorausgesetzt, Sie verfügen über eine richtige Bohrmaschine und sind im Allgemeinen erfahren im Leben). Wenn der Emulator ähnlich ableiten kann, dass das Programm anfordert, ein 32x32-Bild auf dem Bildschirm an einem bestimmten Koordinatensatz zu zeichnen, kann er mit dem Lesen des Codes aufhören, sobald er versteht, um welches Bild es sich handelt, in welchem Format das Bild vorliegt und wo es gezeichnet werden soll – er muss nicht sehen, wie das emulierte System es zeichnet.
So funktionieren Emulatoren
Ein Emulator, der Code für eine andere Plattform und/oder CPU ausführt (zum BeispielWein) führt die Dinge in verschiedenen Phasen aus. Einige Phasen sind unbedingt erforderlich, damit der Emulator funktioniert; andere Phasen sind optional und stellen Möglichkeiten zur Leistungsoptimierung dar.
Erforderlich: „Parsen“ des ausführbaren Codes (Maschinencode, MSIL, Java-Bytecode usw.)Parsingbesteht aus:
- Lesen jedes Bits des ausführbaren Codes.
- Sie müssen das Layout/Format (Syntax) und den Zweck (Semantik) jedes Bits/Bytes (oder jeder anderen diskreten Informationseinheit, die Sie verwenden möchten) des nativen Codes ausreichend verstehen, um zu verstehen, was er tut.
- VerstehenWasein Programm sagt, der Emulator muss verstehen,Syntaxdes Binärformats und derSemantik. Die Syntax besteht aus Dingen wie „wir drücken 32-Bit-Ganzzahlen mit Vorzeichen im Least Signed Bit-Format aus“; die Semantik besteht aus Dingen wie „wenn der native Code einen Opcode enthält52, das heißt, einen Funktionsaufruf durchführen."
- Mnemonische(hilft Ihnen, sich daran zu erinnern, warum dies notwendig ist): Wenn ich mich ganz dem Befolgen eines Rezeptes verschrieben habe, dieses Rezept dann aber völlig ignoriere und nicht einmallesenes ist unmöglich, dass ich dieses Rezept jemals befolgen kann, es sei denn, ich probiere zufällig ein paar Dinge aus undGlückdazu zu bringen, die gleichen Schritte zu unternehmen, die das Rezept erfordern würde. Ebenso muss jeder Emulator verstehen, es sei denn, Sie haben eine randomisierte Monte-Carlo-Simulation, die zufällige CPU-Anweisungen ausführt, bis sie zufällig die gleiche Funktionalität wie das Programm ausführt.Washeißt es im Programm.
Erforderlich: „Übersetzen“ des analysierten Codes (normalerweise eine Art abstraktes Datenmodell, Zustandsmaschine, abstrakter Syntaxbaum oder etwas Ähnliches) in entwederhohes LevelBefehle (z. B. Anweisungen in C oder Java) oderniedriges NiveauBefehle (z. B. CPU-Anweisungen für einen x86-Prozessor). Befehle auf höherer Ebene sind in der Regel optimaler. Wenn Sie beispielsweise den Codefluss einer langen Folge von CPU-Anweisungen analysieren und auf höherer Ebene feststellen, dass die Wiedergabe einer bestimmten MP3-Datei von der Festplatte erforderlich ist, können Sie die gesamte Emulation auf Befehlsebene überspringen und einfach den MP3-Decoder Ihrer nativen Plattform (der möglicherweise für Ihren Prozessor optimiert ist) verwenden, um dieselbe MP3-Datei abzuspielen. Wenn Sie andererseits die Ausführung des emulierten Programms so wörtlich wie möglich „verfolgen“ würden, wäre dies langsamer und weniger optimal, da Sie einen Großteil der Optimierung aufgeben würden, von der Sie profitieren, wenn Sie Anweisungen nativ ausführen.
Optional: „Optimieren“ und Analysieren des Codeflusses eines großen Teils des emulierten Programmcodes oder des gesamten Programms, um die vollständige Ausführungssequenz zu bestimmen, und Erstellen eines sehr detaillierten und ausgefeilten Modells, wie Ihr Emulator dieses Verhalten mit den Möglichkeiten der nativen Plattform emulieren wird. Wine tut dies bis zu einem gewissen Grad, aber es wird dadurch unterstützt, dass der Code, den es übersetzt, x86-zu-x86 ist (was bedeutet, dass die CPU in beiden Fällen denselben Befehlssatz hat, sodass Sie den Windows-Code nur an die fremde UNIX-basierte Umgebung anschließen und ihn „nativ“ ausführen lassen müssen).
Kuchenanalogie
Wenn Sie die Leistung eines Emulators in Betracht ziehen, überlegen Sie, wie viele Blätter Papier Sie benötigen würden, um sich Anweisungen aufzuschreiben, wenn Sie in den folgenden Szenarien jemandem auf einem Video (mit Ton) beim Kuchenbacken zusehen würden:
Wenn Sie noch nie in Ihrem Leben Ihre Hände bewegt oder irgendwelche Muskeln in Ihrem Körper trainiert haben;(Hinweis: Sie benötigen Tausende von Blättern Papier, um die detaillierten Schritte der Handbewegung, Hand-Auge-Koordination, Winkelung, Geschwindigkeit, Position, grundlegender Techniken wie Greifen, Halten von Utensilien, Kneten usw. zu dokumentieren.)
Wenn Sie über eine grundlegende motorische Kontrolle verfügen (Sie können gehen und selbstständig essen), aber noch nie in Ihrem Leben Essen zubereitet haben;(Hinweis: Sie würden Dutzende von Blättern Papier brauchen, um die einzelnen Schritte zu dokumentieren, und wahrscheinlich viel Übung, um Dinge wie das Kneten und Halten unbekannter Utensilien in den Griff zu bekommen, aber Sie würden es in weitaus kürzerer Zeit dokumentieren können als im vorherigen Fall)
Wenn Sie noch nie in Ihrem Leben einen Kuchen gebacken haben, aber schon einmal Lebensmittel zubereitet haben;(Hinweis: Sie benötigen einige Blätter Papier, aber nicht mehr als zehn. Mit dem Abmessen von Zutaten, Umrühren usw. sind Sie bereits vertraut.)
Wenn Sie schon oft einen Kuchen gebacken haben und mit dem Prozess sehr vertraut sind, aber nicht wissen, wie man diese spezielle Sorte/Geschmacksrichtung von Kuchen backt(Tipp: Sie brauchen möglicherweise ein halbes Blatt Papier, um die Grundzutaten und die Backzeit aufzuschreiben, und das war’s).
Grundsätzlich kann der Emulator mit diesen zunehmenden Stufen der „Emulatorkompetenz“ mehr Dinge auf höherer Ebene „nativ“ erledigen (mithilfe von Routinen und Prozeduren, die er bereits kennt) und muss weniger „Tracing“ durchführen (mithilfe von Routinen und Prozeduren, die er wörtlich aus dem emulierten Programm befolgt).
Um diese Analogie in Computerbegriffe zu übertragen, können Sie sich einen Emulator vorstellen, deremuliert die tatsächliche Hardwaredass das emulierte Programm auf dieser Hardware laufen würde und das Verhalten dieser Hardware genau "nachverfolgen" würde, vielleicht sogar bis auf die Hardware-Ebene (Schaltkreisebene); das wäresehrlangsam im Vergleich zu einem Emulator, der das Programm so detailliert analysiert, dass er erkennt, wenn versucht wird, eine Sounddatei abzuspielen, und diese Sounddatei „nativ“ abspielen kann, ohne dazu die Anweisungen des emulierten Programms verfolgen zu müssen.
Über „Nachzeichnen“ (auch bekannt als mechanische Nachahmung) vs. „native Ausführung“
Eine letzte Sache: Das Tracing ist vor allem deshalb langsam, weil Sie viel Speicher verwenden müssen, um sehr detaillierte, komplizierte Komponenten des zu emulierenden Objekts zu „replizieren“. Anstatt die Anweisungen einfach auf Ihrer Host-CPU auszuführen, müssen SieAnweisungen, die die Anweisungen ausführen(sehen Sie den Grad der Indirektion?), was zu Ineffizienz führt. Wenn Sie aufs Ganze gehen und die physische Hardware eines Computersystems sowie das Programm emulieren würden, würden Sie die CPU, das Motherboard, die Soundkarte usw. emulieren, die wiederum die Ausführung des Programms „verfolgen“ würden, so wie Ihr Emulator die Ausführung der CPU „verfolgt“, und mit so vielen Verfolgungsebenen wäre das Ganze extrem langsam und umständlich.
Hier ist ein ausführliches Beispiel, bei dem ein Emulator nicht jedes Bit/Byte des Eingabeprogramms lesen muss, um es zu emulieren.
Nehmen wir an, wir kennen eine API, die in C oder C++ geschrieben ist (die Details sind nicht wichtig) für eine emulierte Softwareumgebung, in der diese API eine Funktion hat void playSound(string fileName)
. Nehmen wir an, wir wissen, dass die Semantik dieser Funktion darin besteht, die Datei auf der Festplatte zu öffnen, ihren Inhalt zu lesen, herauszufinden, in welcher Kodierung die Datei vorliegt (MP3? WAV? etwas anderes?) und sie dann mit der normalen/erwarteten Abtastrate und Tonhöhe über die Lautsprecher abzuspielen. Wenn wir aus dem nativen Code eine Reihe von Anweisungen lesen, die besagen: „Gehen Sie in die Routine playSound ein, um mit der Tonwiedergabe zu beginnen /home/hello/foo.mp3
“, können wir das Lesen des Programmcodes an dieser Stelle beenden und unsereeigen(optimierte!) Routine zum nativen Öffnen und Abspielen dieser Sounddatei. Müssen wir dem emulierten Prozessor auf Befehlsebene folgen? Nein, wirklich nicht, jedenfalls nicht, wenn wir darauf vertrauen, dass wir wissen, was die API macht.
Es entsteht ein krasser Unterschied! (Probleme im hochgelegenen Land)
Wenn Sie eine Reihe von Anweisungen lesen und daraus einen hochrangigen Ausführungsplan „ableiten“, wie im obigen Beispiel, besteht natürlich das Risiko, dass Sie nichtgenauahmen Sie das Verhalten des Originalprogramms nach, das auf der Originalhardware ausgeführt wurde. Nehmen wir zum Beispiel an, die Originalhardware hatte Hardwarebeschränkungen, die nur die gleichzeitige Wiedergabe von 8 Sounddateien ermöglichten. Wenn Ihr neumodischer Computer 128 Sounddateien gleichzeitig problemlos wiedergeben kann und Sie die playSound
Routine auf hohem Niveau emulieren, was hindert Sie dann daran, mehr als 8 Sounddateien gleichzeitig abzuspielen? Dies könnte zu ... seltsamem Verhalten (ob gut oder schlecht) in der emulierten Version des Programms führen. Diese Fälle können durch sorgfältiges Testen oder vielleicht durch ein wirklich gutes Verständnis der ursprünglichen Ausführungsumgebung gelöst werden.
Beispielsweise verfügt DOSBox über eine Funktion, mit der Sie die Ausführungsgeschwindigkeit des emulierten DOS-Programms absichtlich begrenzen können, da einige DOS-Programme nicht richtig ausgeführt würden, wenn man sie mit voller Geschwindigkeit laufen ließe. Tatsächlich sind sie von der Taktfrequenz der CPU abhängig, um mit der erwarteten Geschwindigkeit ausgeführt zu werden. Diese Art von „Funktion“, die die Ausführungsumgebung absichtlich begrenzt, kann verwendet werden, um einen guten Kompromiss zwischenTreueder Ausführung (das heißt, dafür zu sorgen, dass das emulierte Programm richtig funktioniert) undEffizienzder Ausführung (d. h., Erstellen einer Darstellung des Programms, die hoch genug ist, um mit einem Minimum an Ablaufverfolgung effizient emuliert werden zu können).
Antwort2
Meine Frage ist nun: Wenn der Emulator die exakt gleichen Operationscodes emulieren muss wie die echte CPU, wäre es dann nicht erforderlich, das richtige binäre Operationscodeformat eines Spiels zu analysieren, um die CPU legitim (oder überhaupt) zu emulieren?
„Parsen“ bedeutet „Text durchgehen und herausfinden, was er bedeutet“. Text und sprachähnliche Syntax sind komplex, weil nicht nur einzelne „Wörter“ etwas bedeuten, sondern die Bedeutung sich auch je nach ihrer Position und Nähe zu anderen Wörtern ändern kann.
Der wahrscheinlich genauere Begriff für das, was eine CPU mit einem Befehlsstrom macht (was viel einfacher ist als Parsen), ist „Dekodieren“ – und ja, ein Emulator muss Befehle auf die gleiche Weise „dekodieren“. Ich denke jedoch, dass „Parsen“ angesichts der Komplexität des x86-Befehlssatzes gar kein so schlechter Begriff ist, wenn Sie davon sprechen.
Mein Programm muss überprüfen, ob diese Anweisung tatsächlich Bedeutung hat (z. B. „füge eins zum A-Register hinzu“). Aber müsste es dazu nicht jede Null und Eins in der Textdatei überprüfen?
CPUs überprüfen Anweisungen nicht wirklich. Jede CPU hat ein internes Register, das auf einen Speicherplatz im RAM verweist. Die CPU liest diesen, liest bei Bedarf noch ein paar Bytes mehr und versucht dann, ihn auszuführen. Moderne CPUs starten einen „Ausnahmeprozess“, wenn es sich um eine ungültige Anweisung handelt.
Alte CPUs, wie der alte 8-Bit 6502, taten nicht einmal das - einige illegale Anweisungen blockierten die CPU, andere führten seltsame, nicht dokumentierte Dinge aus. (Ein wenig Suchen wird eine Reihe nicht dokumentierter x86-Anweisungen aufdecken, die in allen CPUs im x86-Pantheon zu finden sind - zum Beispiel CPUID
existierte die Anweisung auf einigen 486-CPUs, bevor Intel sie offiziell dokumentierte.)
Ein guter und genauer CPU-Emulator verarbeitet einen Befehlsstrom einfach blind, genau wie eine echte CPU. Allerdings kann er unter Umständen, die zu einem Systemabsturz führen würden, andere Dinge tun, wie z. B. ein Dialogfeld anzeigen, das Sie darüber informiert, dass Ihre virtuelle CPU tot ist, anstatt Ihren Emulator abzustürzen.
Außerdem sagen Sie, „es muss jede Null und Eins in der Textdatei prüfen“, aber normalerweise füttern Sie Emulatoren nicht mit Textdateien. Ein CPU-Emulator benötigt emulierten Speicher – und da viele Plattformen speicherabgebildete E/A verwenden, auch emulierte E/A-Geräte. Um Dinge wie die Firmware zu emulieren, müssen Sie über binäre Bilder dieser Firmware verfügen – entweder legale Kopien der echten Firmware oder Ersatz-Open-Source-Firmware. Um eine ganze Plattform zu emulieren (wie die „PC-Plattform“, zu der die x86-CPU eine Komponente ist), ist mehr als nur CPU-Emulation erforderlich.