Erstellen Sie den am stärksten frequentierten Biber im x86-Maschinencode in maximal 32 Byte

8

Ihre Aufgabe ist es, ein Programm in einer x86-Maschinensprache (eine beliebige Version) zu schreiben, das so viele Anweisungen wie möglich durchläuft und dann mit maximal 32 Byte Code angehalten wird und mit nullen Registern beginnt.

Sie können alles über den Arbeitsspeicher annehmen, den Sie mögen, solange er in einem für den x86-Computer verwendbaren Format vorliegt.

Joe Z.
quelle
3
Es gibt ziemlich kleine beschäftigte Biber-Turing-Maschinen, deren Halteverhalten unbekannt ist ( en.wikipedia.org/wiki/Busy_beaver#Known_values ). Was passiert, wenn jemand einen von ihnen implementiert?
Asmeurer
Es gibt einige Details, die hier ausgefüllt werden könnten. Haben wir einen Stapel für uns eingerichtet? Wie groß ist es? Kann das Programm frei auf den Speicher zugreifen? Wenn ja, wie viel davon? Haben wir Lese- / Schreibzugriff auf alle 4 GB? Wenn ja, können wir auswählen, wo sich unser Programm befindet?
Brotkasten
Sie können alles über den gewünschten Speicher annehmen, solange die tatsächlichen Register zu Beginn der Codeausführung auf Null gesetzt sind. (Wird dies Probleme verursachen? Ich bin nicht besonders erfahren mit Maschinensprache.)
Joe Z.
Können wir annehmen, dass sich der Prozessor im Real-Modus und / oder im geschützten Modus befindet? Wenn wir den geschützten Modus annehmen können, wirft dies eine Reihe weiterer Fragen zur globalen Deskriptortabelle und zur Paging-Tabelle usw. auf. Angesichts dieser Probleme ist es möglicherweise besser, nur den Real-Modus zu erzwingen, was bedeutet, dass viel weniger Speicher verfügbar ist . Es sei denn, wir können einen unwirklichen Modus annehmen, von dem ich jetzt weiß, dass ich implizit davon ausgegangen bin, dass ich mich in diesem Zustand befinde. In jedem Fall sollte dies jedoch in der Problembeschreibung explizit erwähnt werden.
Brotkasten

Antworten:

8

2 524224 Anweisungen

Mein Programm:

_start:
        mov     bl, _end
        stc
iloop:  adc     [bx], al
        inc     bx
        jnz     iloop
        jnc     _start
        a32
        loop    _start
        hlt
_end:

(Technischer Hinweis: Dies ist für nasm geschrieben Die. a32Syntax nasm ist für die alternative Adresse Größe Präfix - Byte Für masm Sie die ersetzen würde. a32Mit defb 0x67.)

Zur Verdeutlichung hier die Ausgabe der Auflistung:

 1                  _start:
 2 0000 B310                mov     bl, _end
 3 0002 F9                  stc
 4 0003 1007        iloop:  adc     [bx], al
 5 0005 43                  inc     bx
 6 0006 75F9                jnz     iloop
 7 0008 73F4                jnc     _start
 8 000A 67                  a32
 9 000B E2F1                loop    _start
10 000D F4                  hlt
11                  _end:

Das Programm geht davon aus, dass sich der Prozessor im Real-Modus befindet und dass sich das Programm am unteren Rand eines 64-KB-Speichersegments befindet, das ansonsten auf All-Bit-Null initialisiert wird. Das Design ist einfach: Behandeln Sie den Speicher als eine einzelne riesige Ganzzahl ohne Vorzeichen und erhöhen Sie ihn um alle möglichen Werte, bis er wieder auf Null gesetzt wird. Wiederholen Sie diese 2 32 Mal. Dann halt an.

Die innerste Schleife (Zeilen 4‒6) ist für die Inkrementierung der großen Ganzzahl verantwortlich. Jede Iteration fügt einem einzelnen Byte null oder eins hinzu, je nachdem, ob das vorherige Byte ausgeführt wurde. Beachten Sie, dass auf jedes Byte in der großen Ganzzahl zugegriffen wird, unabhängig davon, ob es sich geändert hat oder nicht. Daher wiederholt diese Schleife immer 2 16 - 14 Mal.

Übrigens, falls Sie sich fragen, dieser Code zeigt den Grund, warum die x86 inc/ dec-Anweisungen das Übertragsflag nicht beeinflussen: Nur um diese Art von Mehrbyte-Übertragungsmuster zu vereinfachen. (Dieses Muster trat in den Tagen von 8-Bit-Mikroprozessoren häufiger auf, als der ursprüngliche 8080-Befehlssatz definiert wurde.)

Zeile 7 bewirkt, dass der Inkrementierungsprozess wiederholt wird, bis eine Eins aus dem letzten Byte ausgeführt wird, was anzeigt, dass die große Ganzzahl auf alle Bits Null zurückgesetzt wurde. Dies dauert lange.

Die Zeilen 8 bis 9 markieren die äußerste Schleife und bewirken, dass dieser Vorgang 32 Mal wiederholt wird, bis das ecxRegister auf Null rollt. Dies entspricht effektiv dem Hinzufügen weiterer 32 Bit zur riesigen Ganzzahl.

Es wäre möglich, eine weitere äußere Schleife hinzuzufügen und dies erneut zu tun, indem (sagen wir) das edxRegister verwendet wird, und dann möglicherweise esiund edifür noch mehr Wiederholungen. Es lohnt sich jedoch nicht. Die Anweisungen zum Inkrementieren und Schleifen erfordern vier Bytes. Diese vier Bytes werden von der riesigen Ganzzahl entfernt. Wir verlieren also 32 Bit auf dem RAM-Zähler, nur um 32 Bit über ein Register hinzuzufügen: Es wird eine Wäsche. Der einzige Grund ecxfür eine Ausnahme ist, dass es einen speziellen loopBefehl gibt, der nur in drei Bytes passt. Somit tauscht das Programm 24 Bit gegen 32 Bit, eine winzige, aber immer noch positive Verstärkung von 8 Bit.

Es ist nicht allzu schwer, die Anzahl der Anweisungen, die das Programm ausführt, direkt zu berechnen, bevor es angehalten wird. Es gibt jedoch eine viel einfachere Möglichkeit, diese Anzahl zu schätzen. Das Programm ändert seinen gesamten Speicher abzüglich der 14 Bytes, die das Programm enthalten, und der Register bxund ecx. Dies ergibt 2 16 - 14 + 2 + 4 = 65528 Bytes für insgesamt 524224 Bits. Die Verknüpfung beinhaltet die Erkenntnis, dass während des Ausführens jedes einzelne mögliche Muster von 524224 Bits genau einmal erscheint. Für den RAM und das ecxRegister ist dies leicht zu erkennen, da das Programm jeden einzelnen Wert inkrementiert. ZumbxDies ist etwas weniger offensichtlich, da es gleichzeitig mit der Aktualisierung des Speicherwerts geändert wird. Man kann jedoch zeigen, dass sich das Programm angesichts der Struktur des Programms in einer Endlosschleife befinden müsste, wenn ein vollständiges Bitmuster tatsächlich zweimal auftreten würde. Da dies nicht der Fall ist, muss jedes Bitmuster letztendlich nur einmal besucht werden. (Der vollständige Beweis wird natürlich dem Leser als Übung überlassen.)

Da jedes mögliche Bitmuster im Verlauf des Programms auftritt, muss das Programm mindestens 2 524224- Befehle ausführen , was ungefähr 1,4 × 10 157807 entspricht . (Die akute Zahl ist aufgrund der Sprunganweisungen sehr geringfügig höher, aber der Unterschied bei dieser Größe ist vernachlässigbar.)

Offensichtlich könnte dies durch die Verwendung von mehr als 64 KB RAM erheblich verbessert werden. Ich halte die nächste Version des Codes zurück, bis ich genau herausgefunden habe, auf wie viel RAM zugegriffen werden kann.

Brot-Box
quelle
Sind das nicht 17 Anweisungen (34 Bytes)? Oder fehlt mir etwas?
Joe Z.
@ JoeZ. inc ist 1 Byte (wenn Sie es nicht im 16-Bit-Modus zusammenbauen)
kopieren Sie den
Oh. Also wäre dieses Programm tatsächlich 25 Bytes lang?
Joe Z.
Ja, es ist 25 Bytes
Kopie
Entschuldigung, ich hätte expliziter sein sollen. Ich habe die Listenausgabe hinzugefügt, um Mehrdeutigkeiten zu vermeiden.
Brotkasten
4

~ 2 ^ (2 ^ 67) Anweisungen

(at & t Syntax)

start:
    movq    $0x1a,%rax
    stc
loop:
    adcb    %cl,(%rax)
    incq    %rax
    jnz     loop
    jnc     start
    hlt

Zerlegt, 26 Bytes:

start:
0000000000000000    movq    $0x0000001a,%rax
0000000000000007    stc
loop:
0000000000000008    adcb    %cl,(%rax)
000000000000000a    incq    %rax
000000000000000d    jne loop
0000000000000013    jae start
0000000000000019    hlt

Ähnlich wie bei der Breadbox-Lösung, aber es gibt keinen Grund, bei 16 Bit Adresse anzuhalten. Mein Code verwendet x86-64 und geht davon aus, dass wir 2 ^ 64 Byte Speicher haben und alle außer dem Speicher, der den Code enthält, als riesigen Zähler verwenden.

Keith Randall
quelle