Statische Neukompilierung ist eine vielversprechende Möglichkeit, Binärdateien von einer fremden Architektur in eine andere Zielarchitektur zu übersetzen. Es wäre schneller als Just-In-Time (JIT), weil es den Code nicht direkt vor dem Ausführen kompilieren muss, und weil die zusätzliche Kompilierungszeit nützlich ist, um den Generierungscode zu optimieren.
jedoch JIT-Kompilierung verwendet dynamische Programmanalyse, während statische Neukompilierung auf statische Programmanalyse beruht (daher der Name).
In der statischen Analyse haben Sie keine Laufzeitinformationen zu einer Ausführung.
Ein großes Problem mit diesem durch indirekte Sprünge gestellt. Der Begriff umfasst Code, der aus bestimmten switch
-Anweisungen, aus der Verwendung von Funktionszeigern oder aus Laufzeitpolymorphien (think virtual table) generiert werden kann. Es läuft alles auf eine Anweisung der Form nach unten:
JMP reg_A
Angenommen, Sie haben die Startadresse des Programms kennen, und Sie sich entschieden Anweisungen neu kompiliert werden von diesem Punkt zu beginnen. Wenn Sie auf einen direkten Sprung stoßen, gehen Sie zu seiner Zieladresse und Sie setzen die Neukompilierung von dort fort. Wenn Sie jedoch auf einen indirekten Sprung stoßen, stecken Sie fest. In dieser Bauanleitung ist der Inhalt von reg_A
statisch nicht bekannt. Daher kennen wir die Adresse der nächsten Anweisung nicht. Beachten Sie, dass dieses Problem bei der dynamischen Neukompilierung nicht auftritt, da wir den virtuellen Zustand der Register emulieren und den aktuellen Inhalt von kennen. Außerdem sind Sie bei der statischen Neukompilierung daran interessiert, zu diesem Zeitpunkt alle möglichen Werte für reg_A
zu finden, da Sie alle möglichen Pfade kompilieren möchten. In der dynamischen Analyse benötigen Sie nur den aktuellen Wert, um den gerade ausgeführten Pfad zu generieren. Wenn der Wert geändert wird, können Sie weiterhin die anderen Pfade generieren. In einigen Fällen kann die statische Analyse eine Liste von Kandidaten finden (wenn es eine switch
ist, muss es irgendwo eine Tabelle des möglichen Offset geben), aber im allgemeinen Fall wissen wir einfach nicht.
Feine, sagen Sie, sich alle Anweisungen dann in den binären neu kompilieren!
Das Problem hier ist, dass in den meisten Binärdateien sowohl Code und Daten enthalten. Abhängig von der Architektur können Sie möglicherweise nicht feststellen, welche welche ist.
Schlimmer noch, in einigen Architekturen gibt es keine Ausrichtungsbeschränkungen und Befehle mit variabler Breite, und Sie können an einem bestimmten Punkt beginnen, sich zu zerlegen, nur um festzustellen, dass Sie mit einem Offset neu kompiliert haben.
ist eine vereinfachte Befehlssatz, der zwei Befehle und ein einzelnes Register A
Lassen Sie nehmen:
41 xx (size 2): Add xx to `A`.
42 (size 1): Increment `A` by one.
Nehmen wir folgendes Binärprogrammordnung:
41 42
Lassen Sie uns sagen, dass der Startpunkt ist das erste Byte 41
. Sie tun:
41 42 (size 2): Add 42 to `A`.
Aber was, wenn 41 ist ein Stück von Daten? Dann wird Ihr Programm wird:
42 (size 1): Increment `A` by one.
Dieses Problem wird in alten Spielen vergrößert wird, die oft direkt in der Montage optimiert wurde, und wo der Programmierer könnte absichtlich expect some byte to be interpreted as both code and data, depending on the context!
Noch schlimmer könnte das neu kompilierte Programm zu erzeugen Code selbst! Stellen Sie sich vor, Sie würden einen JIT-Compiler neu kompilieren. Das Ergebnis würde immer noch Code für die Quellarchitektur ausgeben und versuchen, dorthin zu springen, was höchstwahrscheinlich dazu führen würde, dass das Programm sehr bald stirbt. Statisches Neukompilieren von Code, der nur zur Laufzeit verfügbar ist, erfordert unendliche Tricks!
Statische binäre Analyse ist ein sehr lebender Bereich der Forschung (hauptsächlich im Bereich der Sicherheit, nach Schwachstellen in Systemen zu suchen, deren Quellen nicht verfügbar sind), und tatsächlich kenne ich einen Versuch, eine NES emulator that tries to statically recompile programs zu produzieren. Der Artikel ist sehr interessant.
Ein Kompromiss zwischen JIT und statischen Neukompilierung wäre statisch so viel Code wie möglich neu zu kompilieren, nur die Bits zu halten, die nicht statisch übersetzt werden kann.
Es ist ein paar Mal getan. Zum Beispiel kompilierte DEC FX! 32 X86-Binärdateien auf einem DEC-Alpha, oft schneller als jedes x86 der Zeit könnte. Es war jedoch nicht genug, um das (falsche) Management von DEC auszugleichen, und Compaq/HP kümmerte sich nicht darum. –
Warum existieren Emulatoren überhaupt, wenn das der Fall ist? Ist das wirklich * viel * schwieriger als einen Emulator zu schreiben? – user1483857