1 Punkte von GN⁺ 2024-07-11 | 1 Kommentare | Auf WhatsApp teilen
  • Beim Neuschreiben eines CPU-Emulators für Time Travel Debugging in C++ zeigte sich, dass x86/amd64 selbst bei gleichem Verhalten je nach Encoding, Präfixen und Ausführungsmodus unterschiedlich behandelt werden muss
  • Es gibt viele alternative Darstellungen mit Auswirkungen auf Performance und Debugging, etwa das einbyteige Encoding CC von int 3, die Kurzform von ADD EAX, imm oder wirkungslose REX-Präfixe
  • INC/DEC, CMPXCHG8B/CMPXCHG16B und Shift-/Rotate-Befehle verarbeiten Flags anders als intuitiv erwartet und führen leicht zu Emulator-Bugs
  • Die Shift-Anzahl wird nicht unverändert in der Operandenbreite angewendet, sondern maskiert, daher setzt shr eax,20h ein 32-Bit-Register nicht auf 0, sondern behält den Wert bei
  • Segmente werden weiterhin für den TEB-Zugriff unter 32-Bit- und 64-Bit-Windows verwendet, und Unterschiede bei Bedeutung von FS/GS sowie der Bestimmung der Base wirken sich direkt auf Disassembler- und Emulator-Implementierungen aus

x86-Details, die beim Neuschreiben des TTD-Emulators sichtbar wurden

  • Eine Komponente von Time Travel Debugging ist ein CPU-Emulator, der die komplette Prozessausführung auf Befehlsebene aufzeichnet
  • Der Emulator der ersten Version, iDNA, war fast vollständig in Assembler geschrieben und schnell, aber schwer zu warten und zu erweitern
  • In der zweiten Version wurden der Emulationsteil und später die meisten anderen Teile in C++ neu geschrieben, mit dem Ziel, den Großteil der Performance der Assembler-Version zu behalten und zugleich eine besser wartbare Codebasis zu schaffen
  • Um einen CPU-Emulator zu bauen, muss man die Details des CPU-Verhaltens lückenlos korrekt nachbilden, und selbst für x86-Erfahrene müssen vertraute Regeln in der realen Implementierung erneut überprüft werden

x86-Encodings: derselbe Befehl auf mehrere Arten

  • x86 kann denselben Befehl als mehrere Bytefolgen darstellen
  • int 3 kann auch als CD 03 codiert werden, aber ebenso als einzelnes Byte CC
    • Da dies für Software-Breakpoints verwendet wird, kann man auch an einer Befehlsposition am Ende einer Speicherseite, deren nächste Seite nicht gemappt ist, einen Breakpoint setzen
  • Es gibt auch alternative Encodings, um häufige Fälle kürzer zu machen
    • add eax, imm lässt sich kürzer als 05cccccccc darstellen
    • Um denselben Wert zu ECX zu addieren, braucht man ein Byte mehr, etwa 81c1cccccccc
  • Dass EAX als „Accumulator register“ bezeichnet wird, ist nicht nur Konvention, sondern führt tatsächlich zu Unterschieden im Encoding, und kürzere Befehle können für die Performance vorteilhaft sein, weil weniger Daten aus dem Hauptspeicher geholt werden müssen und der Instruktionscache sparsamer genutzt wird
  • Compiler können solche kurzen Encodings nutzen, wenn es möglich ist

Präfixe und das 15-Byte-Limit für Befehlslängen

  • x86-Befehle können Präfix-Bytes haben, die ihr Verhalten verändern
  • Das im 64-Bit-Code häufig genutzte REX-Präfix dient dazu, auf einen größeren Registerbereich als im 32-Bit-Code zuzugreifen
  • Die CPU akzeptiert auch REX-Präfixe ohne Wirkung
    • 4004cc ist eine Form mit einem REX-Byte vor dem 8-Bit-add al,0CCh, aber in diesem Fall hat das REX keine Wirkung
    • Selbst zwei REX-Präfixe hintereinander kann die CPU ausführen, und mehrere Disassembler einschließlich WinDbg können dadurch verwirrt werden
  • Auf x86-kompatiblen CPUs ist die aktuelle Befehlslänge bei 15 Byte hart begrenzt
    • Befehle mit mehr als 15 Byte gelten als ungültige Befehle und lösen eine Ausnahme aus
    • Ältere CPUs haben auch andere Beschränkungen bei Präfixen, und es gibt Präfixe wie LOCK, bei denen die Einsatzbedingungen strenger sind

Unterschiedliche Interpretation je nach Adressgröße und Modus

  • Das Address-override-Präfix kann im 64-Bit-Modus dazu führen, dass 32-Bit-Adressen referenziert werden
    • 488d0424 ist lea rax,[rsp]
    • 67488d0424 ist wegen des Präfixes 0x67 lea rax,[esp]
  • Im 32-Bit-Code schaltet Address override den Adressmodus auf 16-Bit-Adressierung um
  • Dieselbe Bytefolge lässt sich nur korrekt disassemblieren oder interpretieren, wenn man die Standard-Operanden- und Adressgröße des Codesegments kennt
    • 8b0424 ist im 32-Bit-Modus mov eax,dword ptr [esp]
    • 8b0424 ist im 64-Bit-Modus mov eax,dword ptr [rsp]
  • Der Bereich 40~4F, der auf x86 für INC reg und DEC reg verwendet wurde, dient auf x64 als REX prefix bytes
    • Im 32-Bit-Modus wird 48 03 04 24 als zwei Befehle interpretiert: dec eax und add eax,dword ptr [esp]
    • Im 64-Bit-Modus wird 48030424 als ein einziger Befehl add rax,qword ptr [rsp] interpretiert
  • Die Designer von AMD64 nutzten den breiten Encoding-Raum von INC/DEC, um im 64-Bit-Modus mit neuen Präfixen den Registersatz zu erweitern; für diese Befehle gab es bereits andere Encodings, die sowohl Register als auch Speicher unterstützten

Die 64-Bit-Falle von WinDbg bei INC reg

  • WinDbg assembliert Befehle immer so, als wären sie im 32-Bit-Modus, weshalb der Versuch, INC reg in 64-Bit-Code zu assemblieren, zu einem anderen Ergebnis als beabsichtigt führen kann
  • Im Beispiel wird inc eax nicht zu einem echten Inkrementbefehl, sondern zu einem nutzlosen REX-Präfix, das den folgenden Befehl modifiziert
  • Dadurch wird die Bytefolge nicht als inc, sondern als Präfix vor einem jmp-Befehl interpretiert

Ausnahmen beim Flag-Verhalten

  • INC EAX sieht ADD EAX, 1 ähnlich, ist aber nicht vollständig identisch
    • ADD aktualisiert das Carry-Flag
    • INC aktualisiert das Carry-Flag nicht
  • Bei der Implementierung des TTD-Emulators wurde dieser Unterschied zunächst falsch umgesetzt und durch Unit-Tests entdeckt
  • Die meisten arithmetischen und logischen Operationen setzen Overflow-, Sign-, Zero-, Auxiliary-Carry-, Parity- und Carry-Flag
  • CMPXCHG setzt diese Flags ebenfalls, aber CMPXCHG8B und CMPXCHG16B ändern nur das Zero-Flag
  • Einige Befehle lassen bestimmte Flags undefiniert
    • Shift- und Rotate-Befehle lassen das Overflow-Flag undefiniert, wenn die Shift-Anzahl größer als 1 ist
    • Das reale Verhalten undefinierter Flags hängt mit der internen Implementierung der Shift-Operation zusammen und kann sich je nach Architektur unterscheiden
    • Es heißt, CPUs der Atom-Familie führten Bit-Shifts in der ALU auf eine billigere und langsamere Weise aus, wodurch andere undefinierte Flag-Werte entstehen, getestet wurde das aber nicht direkt

Count-Masking bei Shift-Befehlen

  • 66c1e810 ist shr ax,10h und shiftet AX um 16 Bit nach rechts
    • Da AX ein 16-Bit-Register ist, wird das Ergebnis 0
  • c1e820 ist shr eax,20h und sieht oberflächlich wie ein Befehl aus, der EAX um 32 Bit nach rechts shiftet
  • Tatsächlich ändert sich der Wert von EAX nicht
    • Laut Intel SDM wird der Count mit 1Fh maskiert, sodass nur die unteren 5 Bits der Rotation verwendet werden
    • Mit dem Präfix REX.W wird die Maske zu 3Fh, womit der maximale Shift-Wert 63 Bit beträgt
  • In einem Microsoft-Bewerbungsgespräch wurde dieses Verhalten tatsächlich relevant bei der Frage nach „allen Möglichkeiten, ein 32-Bit-Register mit einem einzigen Befehl zu löschen“
    • Der Interviewer meinte, das gehe mit einem Shift, die Antwort lautete jedoch, dass das bei einem 32-Bit-Register nicht möglich ist

Segmente leben auch in 32-Bit- und 64-Bit-Code weiter

  • Segmentierter Speicher wirkt wie ein Relikt aus 16-Bit-Code, hat aber auch in 32-Bit- und 64-Bit-Code reale Wirkung
  • Die meisten Betriebssysteme verwenden ein fast flaches Speichermodell und setzen die Segment-Base-Adresse auf 0, weshalb man sich dessen im Alltag kaum bewusst ist
    • Im 64-Bit-Modus behandelt die CPU die Segment-Base von CS, DS, ES und SS immer als 0
  • Eine Ausnahme ist Thread Local Storage, wofür zusätzliche Segmentregister wie FS oder GS verwendet werden
  • Als Korrekturhinweis: Die Base der Segmente FS/GS kann auch aus unprivilegiertem Code mit den Befehlen rdfsbase, wrfsbase, rdgsbase, wrgsbase gelesen werden
    • Diese Befehle sind seit Ivy Bridge, also seit 2012, verfügbar

Windows-TEB-Zugriff und FS/GS

  • Unter Windows werden FS und GS verwendet, um auf den TEB (Thread Execution Block) zuzugreifen
  • Die TEB-Struktur enthält einen Self-Pointer, der auf die flache Adresse des Beginns der Struktur zeigt, und diese Adresse ist zugleich die Base des jeweiligen Segments
  • In 32-Bit-Prozessen liegt der TEB in FS
    • GetLastError holt TEB.NtTib.Self aus fs:[00000018h] und liest dann LastErrorValue aus [eax+34h]
  • In 64-Bit-Prozessen liegt der TEB in GS
    • GetLastError liest den Pointer aus gs:[30h] und holt den Wert aus [rax+68h]
  • 32-Bit-Prozesse, die auf einem 64-Bit-Betriebssystem laufen, besitzen sowohl einen 32-Bit-TEB als auch einen 64-Bit-TEB, und es gibt nützliche Kontexte, in denen man auf beide zugreifen muss, etwa für 64-Bit-WOW-Code, der innerhalb eines 32-Bit-Prozesses ausgeführt wird

Auch die Bestimmung der Segment-Base hängt vom Modus ab

  • Die CPU-Konfiguration zur Bestimmung der Base-Adresse von FS und GS unterscheidet sich zwischen 32-Bit- und 64-Bit-Modus
  • Im 32-Bit-Modus verweist der tatsächliche Wert des Segmentregisters auf einen Segment-Descriptor, der in der Global Descriptor Table und der Local Descriptor Table definiert ist
  • Im 64-Bit-Modus wird die Base durch zwei MSRs gesteuert
    • FS Base, im Intel SDM IA32_FS_BASE
    • GS Base, im Intel SDM IA32_GS_BASE
  • Deshalb ist im 64-Bit-Modus der tatsächliche Registerwert von FS und GS selbst nicht wichtig
    • Entscheidend ist das Segment-override-Präfix
  • Beim Debuggen eines 32-Bit-Prozesses in WinDbg kann man mit dem Wert des FS-Registers den Inhalt des „FS-Segments“ dumpen
  • In 64-Bit-Prozessen funktioniert das auf dieselbe Weise nicht, dort ist das Segment-override-Präfix wichtiger als der Segmentwert

Praktische Lehren für Emulator-Implementierer

  • Wer einen x86-Emulator baut, muss sich detailliert mit dem realen Verhalten der CPU bei Instruktions-Encoding, Präfixen, Flags, Shift-Counts und Segmenten befassen
  • Viele dieser Regeln sind beim normalen Schreiben von Code fast nutzlos, für die Emulator-Implementierung aber direkte Anforderungen
  • Vieles wurde durch Versuch und Irrtum sowie Mentoring gelernt; außerdem werden Darek Mihocka mit seiner Erfahrung aus alten Emulatoren und emulators.com erwähnt
  • Wer sich für x86-Optimierung und Low-Level-Verhalten interessiert, findet nützliches Material auf Agner Fog’s website

1 Kommentare

 
GN⁺ 2024-07-11
Hacker-News-Kommentare
  • Nebenbei gibt es auch bei BSF/BSR Besonderheiten. Das Intel SDM sagt, dass der Zielwert undefiniert ist, wenn die Eingabe 0 ist, während AMD dokumentiert, dass das Ziel in diesem Fall nicht verändert wird.
    glibc nutzt jedoch die undokumentierte Tatsache, dass das Ziel auch bei Intel nicht verändert wird [1]. Deshalb hat es ziemlich lange gedauert, die Ursache des Problems in meinem Binary Translator zu finden.
    Außerdem sind TZCNT/LZCNT BSF/BSR-Codierungen mit F3-Präfix; auf älteren Prozessoren, die diese Erweiterung nicht unterstützen, wird das Präfix stillschweigend ignoriert. Dadurch verhält sich derselbe Code je nach CPU unterschiedlich, aber wenigstens ist das dokumentiert.
    Bei der Codierung schimpfen Leute oft über Präfixe, aber persönlich finde ich, dass sie nicht das Schlimmste sind. Sie sind bekannt und bis zu einem gewissen Grad dokumentiert. Es gibt schlimmere Eigenheiten. Zum Beispiel werden die REX/VEX/EVEX.RXB-Erweiterungsbits ignoriert, wenn sie nicht anwendbar sind, lösen bei den Maskenregistern k0-k7 aber #UD aus. Wenn das Register jedoch in ModRM.rm codiert ist, werden die Erweiterungsbits wiederum ignoriert.
    APX hebt die Eigenheiten noch eine Stufe höher. Das REX2-Präfix kann die Allzweckregister r16-r31 codieren, aber nicht xmm16-xmm31; das EVEX-Präfix hat je nach Opcode mehrere Layouts, und die für Register verwendeten Erweiterungsbits unterscheiden sich ebenfalls je nach Registertyp. XMM-Register verwenden X3:B3:rm und V4:X3:idx, Allzweckregister verwenden B4:B3:rm und X4:X3:idx. Ein Jahr ist vergangen, aber ich habe den APX-Decoder immer noch nicht fertig, daher kann ich keine vollständige Liste liefern.
    [1]: https://sourceware.org/bugzilla/show_bug.cgi?id=31748

    • Im Lauf des letzten Jahres habe ich nebenbei den x86-Decoder von QEMU neu geschrieben. Anfangs war das nötig, um AVX-Unterstützung einzubauen, aber inzwischen bleiben nur noch wenige Opcodes zum Umschreiben übrig, und danach dürfte APX-Unterstützung wohl auch nicht mehr allzu schwierig sein.
      Bei EVEX will ich so vorgehen, dass ich die Rohbits beibehalte, bis der Opcode gelesen und die EVEX-Klasse identifiziert ist. Also ist geplant, sie bis vor den Immediate-Wert, vermutlich bis vor ModRM, zu erhalten.
      Mein Decoder basiert größtenteils auf den Tabellen im Handbuch, und der Code ist im Großen und Ganzen in Ordnung. Er ist nicht übermäßig eingerückt, und die Schritte sind meist getrennt oder leicht zu erkennen. Da die Ausgabe JIT-Code ist, muss er nicht extrem effizient sein; Lesbarkeit ist wichtiger. Dort wird auch nicht der Großteil der Zeit verbracht.
      Trotzdem gibt es mehrere Fälle, in denen das Handbuch falsch ist oder nicht alles sagt. Die Tabellen wurden seit Jahren nicht aktualisiert, und zum Beispiel fehlen auch die K-Register-Instruktionen. Künftig wird wohl mehr Handarbeit nötig sein.
      Der Kommentar ganz oben erklärt die Situation ein wenig: https://github.com/qemu/qemu/blob/59084feb256c617063e0dbe7e6...
      Wie oben gesagt, gibt es noch ein paar Instruktionen, die der alte Code verarbeitet, insbesondere BT/BTS/BTR/BTC. Der Code ist geschrieben, aber noch nicht gemergt.
    • Es gibt noch eine weitere Eigenheit aus der 486- und Pentium-Zeit. BSWAP EAX wandelt zwischen Little Endian und Big Endian um und war von Anfang an eine 32-Bit-Instruktion.
      Es gibt jedoch das Präfix 0x66, das zwischen 16-Bit- und 32-Bit-Modus umschaltet. Wenn man es auf BSWAP EAX anwendet, passiert etwas Undefiniertes und Seltsames.
      Auf manchen CPU-Architekturen wurde das Präfix, ähnlich wie bei Unterschieden zwischen Intel und AMD, einfach ignoriert; auf anderen führte es zu einem Verhalten, das ich „interner Swap“ nenne. Zum Beispiel wurden von den vier in EAX gespeicherten Bytes Byte 1 und Byte 2 miteinander vertauscht.
      0x11223344 wird zu 0x11332244.
    • Wenn man daran denkt, dass all diese Logik in Silizium korrekt und obendrein schnell funktionieren muss, wird einem schwindlig.
      Früher war x86 Intels Burggraben, heute wirkt es eher wie eine albtraumhafte Last, die man mitschleppen muss.
    • Die Kombination aus Semantik und Codierung von LZCNT fühlt sich wie ein Eigentor an. Es ist als BSR-Instruktion mit einem Präfix codiert, das bei Legacy-Code ignoriert wird; bei einer nicht nullwertigen Eingabe ist der Rückgabewert die Operandengröße minus dem Rückgabewert der Legacy-Version.
      Zwar gibt es eine Funktion namens clz(), aber wenn LZCNT einfach BSR mit nur anderer Semantik für Null-Eingaben gewesen wäre, wäre eine zusätzliche Subtraktion in der Implementierung wohl ein geringer Preis gewesen, um Kompatibilität zu erreichen.
    • Ich kenne mich in diesem Bereich nicht gut aus, aber es wäre interessant, ein JTAG-Interface an eine x86-CPU anzuschließen, sie instruktionsweise auszuführen und alle Registerwerte aufzuzeichnen.
      Wenn man dasselbe Programm auch auf einer emulierten CPU ausführt und nach jeder Instruktion prüft, ob der Zustand identisch ist, könnte man testen, ob der Emulator die Hardware perfekt nachbildet.
  • Ein beeindruckender Mensch. Assembly zu schreiben fühlt sich schlicht an, und ich mag auch die vertikal angeordnete Ästhetik.
    Das, was meiner Erfahrung nach der Arbeit des OP auch nur etwas nahekam, war, dass ich einem Freund, der JS macht, den Stack erklären wollte und wir zusammen eine Mini-VM mit einer kleinen ISA gebaut haben: https://gist.github.com/darighost/2d880fe27510e0c90f75680bfe...
    Man hätte tiefer einsteigen können, und ich wünschte, ich hätte das getan, aber dann wären wir wohl vom ursprünglichen Lernziel abgekommen. Ich sollte den Freund mal anschreiben und fragen, ob er noch zusammen lernen will. Er verdient mit cooler Webentwicklung viel Geld und hat keine Zeit, tief einzutauchen; ich bin arbeitslos und habe gefühlt ein nahezu unendliches Meer an Zeit und Energie, also ist das nicht ganz einfach.

    • Vielleicht wäre das ein guter Anlass, von diesem Freund JS zu lernen.
    • Als Nicht-Informatiker war dieser Code für mich ein guter Einstieg, um die internen Abläufe zu verstehen, und daraus wurde eine ziemlich coole und schwierige Assembly-Reise. Ich habe vor, noch tiefer einzusteigen.
  • Justine Tunney und ihr Emulator sind ebenfalls sehenswert. https://justine.lol/blinkenlights/
    Die Dokumentation bietet einen hervorragenden Rundgang dazu, wie eine CPU funktioniert.

    • Erstaunlich. Sie bringt mich jedes Mal zum Staunen.
    • Den Namen Tunney verbinde ich mit der Zeit um 2014, als sie obdachlos umherzog und auf Twitter irgendwelchen Occupy-bezogenen Unsinn schrieb.
  • Die frühere Diskussion ist hier: https://news.ycombinator.com/item?id=34636699
    Kaum zu glauben, dass schon 16 Monate vergangen sind. Die Zeit vergeht wirklich schnell.

  • Ich stimme entschieden nicht zu, dass „das Schreiben eines CPU-Emulators der beste Weg ist, die Funktionsweise einer CPU wirklich zu verstehen“.
    Der beste Weg ist, wie in einem guten Informatikkurs, eine CPU auf Gate-Ebene zu bauen. Es hat wirklich Spaß gemacht, einen verkleinerten ARM von Grund auf zu bauen.

    • Ich denke, beides ist nützlich. Allerdings ist das Entwerfen einer modernen CPU auf Gate-Ebene für die meisten außer Reichweite, und zwischen einer CPU, die man an der Uni entwirft, und einer CPU, auf der echter Code läuft, klafft eine große Lücke.
      Einen modernen CPU-Emulator zu bauen ist eine etwas zugänglichere Herausforderung, und selbst wenn nur Teile funktionieren, hat es immer noch einen großen Lerneffekt.
    • Zustimmung. Selbst ein einfacher Prozessor mit Mikrocode, Pipeline, Superskalarität, Branch Prediction, L1-Daten-/Instruktionscache und Write-back-L2-Cache-Controller ist nicht ohne.
      Die meisten Software Engineers haben ein unvollständiges Verständnis von Data Hazards, Cache-Invalidierung und Pipeline-Stalls.
    • Ich denke, beides stimmt. 74xx-Chips zu verdrahten ist enorm befriedigend, und man bekommt ein Gefühl für die elektrischen Aspekte und die internen Kompromisse.
      Aber sobald man anfängt, auf diese Weise eine CPU zu bauen, die man für sinnvolle Aufgaben verwenden möchte, werden diese Details weniger interessant. Dann werden das Verhalten und die Komplexität der Spezifikation spannender, und der Emulator-Ansatz ist handhabbarer und kann mehr Arten von Verhalten abdecken.
    • Ich arbeite mich gerade mit Nand2Tetris von der Gate-Ebene nach oben vor und habe eben das Kapitel zum VM-Emulator abgeschlossen; das hat wirklich lange gedauert. Jetzt geht es weiter zur Compiler-Phase.
    • Umgekehrt gefragt: Würde man in einer CPU auf Gate-Ebene wirklich bis zur Speichersegmentierung gehen? Ich denke, für echtes Verständnis braucht es beide Schritte: erst eine tatsächlich funktionierende CPU bauen und dann eine reale CPU samt ihrer Macken emulieren.
  • Ich habe schnelle Emulatoren für etwa ein Dutzend nicht-spielzeughafte Architekturen geschrieben und auch einige JIT-Übersetzer gebaut, aber x86 verursacht mir immer noch PTSD. Ich habe noch nie eine derart chaotische Architektur gesehen. Sie hat Geschichte und Gründe, aber trotzdem ist es wirklich heftig.

    • Die x86-Architektur zu lernen fühlt sich an wie das Lernen einer Sprache mit vielen Unregelmäßigkeiten, vestigialen Organen und konkurrierenden Syntaxsystemen, etwa Französisch. Andere Architekturen wie RISC-V oder ARMv8 sind deutlich konsistenter.
    • Wenn du „noch nie eine chaotischere Architektur gesehen“ hast: Es gibt Itanium. Jedes Mal, wenn ich das Handbuch aufschlage, entdecke ich, ohne gezielt danach zu suchen, etwas Neues, bei dem ich denke: „Was zur Hölle haben sie sich dabei gedacht?“
    • Kann ich nachvollziehen. Vielleicht hätte ich wahrscheinlich von Anfang an damit beginnen sollen.
  • Ich habe kürzlich als Nebenprojekt einen beträchtlichen Teil eines x86-64-Decoders implementiert [1] und war ziemlich überrascht, wie viel komplexer das inzwischen geworden ist. Für meinen Zweck war Sandpile.org [2] wirklich nützlich.
    [1] Genau genommen die x86-64-Version von Fabian Giesens disfilter, die ich für ein weiteres, noch nicht veröffentlichtes Nebenprojekt erstellt habe: https://gist.github.com/lifthrasiir/df47509caac2f065032ef72e...
    [2] https://sandpile.org/

  • Der 68k-Disassembler, den ich an der Uni gebaut habe, war wie der Moment, in dem Neo sagt: „Ich kann Kung-Fu.“ Er war das fehlende Bindeglied, das mich vom Hochsprachen-Code bis hinunter zu Transistoren und wieder zurück über Code nachdenken ließ.
    Einen vollständigen Emulator zu schreiben dürfte noch einmal eine Größenordnung wirkungsvoller sein. Guter Artikel.

    • Ich denke, das Schreiben eines ISA-Emulators hilft tatsächlich nicht besonders dabei, zu verstehen, wie moderne Superskalar-CPUs funktionieren. Fast alles daran sind intern verborgene Optimierungen.
  • Meine Erinnerung war wohl falsch. Ich dachte, die Salsa20-Variante und der Maschinencode seien ursprünglich auf cryp.to gewesen, aber Dan Bernsteins Website war https://cr.yp.to/.
    In der Zeit, als wir in einem Startup Verschlüsselung ruhender Daten, Streaming-Verschlüsselung usw. evaluierten, hatte Dan auf seinen Seiten mehrere Implementierungen je nach Ziel-Chipsatz und Instruktionssatz. Sie waren aus seiner Assembler-Darstellung cross-kompiliert.
    Es war interessant, mit der VM auszuprobieren, welche Instruktionssätze Anfang bis Mitte der 2000er unterstützt wurden. Beim Testen gab es gelegentlich Probleme, weil die VM zwar behauptete, etwas zu unterstützen, die Implementierung aber nicht vollständig war.

    • Du meinst wohl die Website von Dan Berstain … Moment mal.
  • Ich finde es interessant, wie viel hier darüber gesprochen wird, wie schmerzhaft x86-Assembly im Vergleich zu RISC ist. Ich habe beim erneuten Aufteilen von Code in Objektdateien das genau gegenteilige Problem.
    Für diesen Zweck ist x86-Analyse wirklich einfach, während MIPS ein Albtraum war. Hauptsächlich, weil es mir um Referenzen auf Code und Daten geht. x86 hat Pointer-große Immediate-Konstanten, MIPS dagegen HI16/LO16-Relocation-Paare, die sich mit Register-Use-Graphs, Codefluss und Branch-Delay-Instruktionen verheddern und alle möglichen Probleme verursachen.
    Damit will ich x86 aber nicht loben.

    • Stimmt. x86 ist seltsam, aber Instruktionen variabler Länge sehen, sobald man sie in Textform aufgelöst hat, eigentlich ganz gut aus und sind auch leicht zu verstehen. Das Problem ist, dass man mitten in einer Instruktion andere Instruktionen verstecken kann, was unsicher ist.
      Eine der wichtigsten Erkenntnisse beim Vergleich von x86-Assembly und C ist, dass signed/unsigned keine Eigenschaft eines Typs ist, sondern einer Operation.
      Es wäre schön, Flags nutzen zu können, und bei manchen Architekturen wie PPC oder armv7 ist das einfacher, aber x86 überschreibt Flags so leicht, dass es viel zu schwer ist, ihre Werte zu nutzen.