Fort Byte Car (und mehr) Indianer

Bild

Ja, ja, es ist das "Byte" und es ist in indisch (nicht indisch). Ich werde in der richtigen Reihenfolge beginnen. Kürzlich erschienen hier auf Habré Artikel über Bytecode. Und es war einmal ein Spaß, Fort-Systeme zu schreiben. Natürlich im Assembler. Sie waren 16-Bit. Ich habe nie auf x86-64 programmiert. Auch mit 32 konnte nicht gespielt werden. Also kam der Gedanke - warum nicht? Warum nicht das 64-Bit-Fort und sogar den Bytecode aufrühren? Ja, und unter Linux, wo ich auch nichts System geschrieben habe.

Ich habe einen Heimserver mit Linux. Im Allgemeinen habe ich ein bisschen gegoogelt und herausgefunden, dass der Assembler unter Linux GAS und der Befehl as heißt. Ich verbinde mich über SSH mit dem Server und tippe als - ja! Ich habe es bereits installiert. Benötigen Sie noch einen Linker, geben Sie ld ein - ja! Also, und versuchen Sie, etwas Interessantes in Assembler zu schreiben. Ohne Zivilisation nur ein Wald, wie echte Indianer :) Ohne Entwicklungsumgebung nur eine Kommandozeile und Midnight Commander. Der Editor wird Nano sein, der an meinem F4 in mc hängt. Wie singt die Gruppe „Zero“? Ein wahrer Inder braucht nur eines ... Was braucht ein echter Inder noch? Natürlich ein Debugger. Wir tippen gdb - is! Drücken Sie Umschalt + F4 und los geht's!

Architektur


Entscheiden wir uns zunächst für die Architektur. Bei bereits ermittelten Bittiefen 64 Bit. In klassischen Fort-Implementierungen sind Daten und Codesegment identisch. Aber wir werden versuchen, es richtig zu machen. Wir haben nur den Code im Codesegment, die Daten im Datensegment. Als Ergebnis erhalten wir einen Kernel für die Plattform und einen vollständig plattformunabhängigen Bytecode.

Versuchen wir, die schnellste gestapelte Byte-Maschine zu erstellen (jedoch ohne JIT). Wir werden also eine Tabelle mit 256 Adressen haben - eine für jeden Bytebefehl. Weniger als alles andere - eine zusätzliche Überprüfung, dies sind 1-2 Prozessoranweisungen. Und wir brauchen schnell und ohne Kompromisse.

Stapel


Normalerweise wird in Fort-Implementierungen der Prozessor-Rückgabestapel (* SP) als Datenstapel verwendet, und der Fort-System-Rückgabestapel wird auf andere Weise implementiert. In der Tat wird unsere Maschine gestapelt, und die Hauptarbeit liegt auf dem Datenstapel. Machen wir also dasselbe - RSP wird ein Datenstapel sein. Nun, der Rückgabestapel sei RBP, was standardmäßig auch mit dem Stapelsegment funktioniert. Wir werden also drei Speichersegmente haben: ein Codesegment, ein Datensegment und ein Stapelsegment (es wird sowohl einen Datenstapel als auch einen Rückgabestapel haben).

Register


Ich gehe in die Beschreibung der Register x86-64 und hoppla! Es gibt bis zu 8 zusätzliche Allzweckregister (R8 - R16) im Vergleich zu 32- oder 16-Bit-Modi. Nicht schlecht ...

Bereits entschieden, dass sie RSP und RBP benötigen. Benötigen noch einen Zeiger (Zähler) der Bytecode-Befehle. Von Operationen in diesem Register wird nur das Lesen des Speichers benötigt. Die Hauptregister (RAX, RBX, RCX, RDX, RSI, RDI) sind flexibler, universeller, mit ihnen gibt es viele spezielle Befehle. Sie werden uns für verschiedene Aufgaben nützlich sein, und für den Bytecode-Anweisungszähler nehmen wir eines der neuen Register für mich, sei es R8.

Fangen wir an


Ich habe keine Erfahrung in der Programmierung unter Linux in Assemblersprache. Daher finden wir zunächst die fertige "Hallo Welt", um zu verstehen, wie das Programm startet und den Text anzeigt. Unerwartet für mich fand ich Optionen mit einer seltsamen Syntax, bei denen sogar die Quelle und der Empfänger neu angeordnet wurden. Wie sich herausstellte, ist dies die AT & T-Syntax, die hauptsächlich unter GAS darauf geschrieben ist. Es wird jedoch eine andere Syntaxoption unterstützt, die als Intel-Syntax bezeichnet wird. Ich dachte nach und beschloss, es trotzdem zu benutzen. Schreiben Sie am Anfang von .intel_syntax noprefix.

Kompilieren Sie "Hallo Welt" und führen Sie es aus, um sicherzustellen, dass alles funktioniert. Durch das Lesen der Hilfe und der Experimente begann ich, den folgenden Befehl zum Kompilieren zu verwenden:
$ as fort.asm -o fort.o -g -ahlsm >list.txt
Hier gibt der Schalter -o die Ergebnisdatei an, der Schalter -g weist an, Debugging-Informationen zu generieren, und der Schalter -ahlsm legt das Listenformat fest. Und ich behalte die Ausgabe in der Liste, darin können Sie viele nützliche Dinge sehen. Ich gebe zu, zu Beginn der Arbeit habe ich die Auflistung nicht durchgeführt und nicht einmal den Schalter -g angegeben. Ich begann den Schalter -g nach der ersten Verwendung des Debuggers zu verwenden und begann mit der Auflistung, nachdem die Makros im Code erschienen waren :)

Danach verwenden wir den Linker, aber hier ist nirgendwo einfacher:

$ ld forth.o -o forth
Nun, lauf!
$ ./forth
Hello, world!

Es funktioniert.

Dies war der erste vierte (tatsächlich ist es 'Hellow, Welt!', Natürlich)
 .intel_syntax noprefix .section .data msg: .ascii "Hello, world!\n" len = . - msg #  len    .section .text .global _start #     _start: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, OFFSET FLAT:msg #     mov edx, len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit xor ebx, ebx #    0 int 0x80 #   

Übrigens habe ich später herausgefunden, dass es in x86-64 korrekter ist, syscall für einen Systemaufruf zu verwenden, als int 0x80. Der 0x80-Aufruf wird für diese Architektur als veraltet angesehen, obwohl er unterstützt wird.

Ein Anfang wurde gemacht, und jetzt ...

Lass uns gehen!


Daß es zumindest einige Besonderheiten geben würde, werden wir den Code eines Byte-Befehls schreiben. Sei es das Fort-Wort "0", das 0 auf den Stapel legt:

 bcmd_num0: push 0 jmp _next 

Zum Zeitpunkt der Ausführung dieses Befehls zeigt R8 bereits auf den nächsten Bytebefehl. Es ist notwendig, es zu lesen, R8 zu erhöhen, die ausführbare Adresse durch den Code des Bytebefehls zu bestimmen und die Steuerung darauf zu übertragen.

Aber ... welche Bittiefe wird die Bytebefehls-Adresstabelle haben? Dann musste ich mich für mich in das neue x86-64-Befehlssystem einarbeiten. Leider habe ich keine Befehle gefunden, mit denen Sie zum Offset im Speicher wechseln können. Berechnen Sie also entweder die Adresse, oder die Adresse ist bereit - 64 Bit. Wir haben keine Zeit zum Berechnen, was bedeutet - 64 Bit. In diesem Fall beträgt die Tabellengröße 256 * 8 = 4096 Byte. Nun, schließlich codieren Sie den _next-Aufruf:

 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] # bcmd -   - 

Nicht schlecht, scheint mir ... Es gibt nur drei Prozessoranweisungen, wenn von einem Bytebefehl zu einem anderen gewechselt wird.

Tatsächlich waren diese Befehle für mich nicht so einfach. Ich musste mich erneut mit dem Befehlssystem 0x86-64 befassen und einen neuen MOVZX-Befehl für mich finden. Tatsächlich konvertiert dieser Befehl einen Wert von 8, 16 oder 32 Bit in ein 64-Bit-Register. Es gibt zwei Varianten dieses Befehls: vorzeichenlos, wobei die höheren Ziffern mit Nullen aufgefüllt werden und die vorzeichenbehaftete Ziffer MOVSX ist. In der vorzeichenbehafteten Version wird das Vorzeichen erweitert, dh bei positiven Zahlen werden Nullen auf die höheren Ziffern und bei negativen auf Einsen gesetzt. Diese Option ist auch für den Befehl lit byte nützlich.

Ist diese Option übrigens die schnellste? Vielleicht schlägt jemand noch schneller vor?

Nun haben wir eine Byte-Maschine, die eine Folge von Byte-Befehlen durchlaufen und ausführen kann. Es ist notwendig, es in der Praxis zu testen, um zu zwingen, mindestens ein Team auszuführen. Aber welches? Null auf dem Stapel? Aber hier kennen Sie nicht einmal das Ergebnis, wenn Sie sich den Stapel unter dem Debugger nicht ansehen ... Aber wenn das Programm gestartet wurde, kann es abgeschlossen werden :)

Wir schreiben einen Tschüss-Befehl, der das Programm vervollständigt und darüber schreibt, zumal wir "Hellow, Welt!" Haben.

 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   

Sie müssen nur noch eine Adresstabelle für Bytebefehle erstellen, die Register initialisieren und die Byte-Maschine starten. Also ... es gibt 256 Werte in der Tabelle und es gibt zwei Befehle. Was ist in den anderen Zellen?
Der Rest hat einen ungültigen Operationscode. Sie können dies jedoch nicht überprüfen. Dies sind zusätzliche Teams. Wir haben jetzt drei und mit dem Scheck sind es fünf. Also werden wir so einen Stub-Befehl machen - ein schlechtes Team. Zuerst füllen wir die gesamte Tabelle dafür aus und beginnen dann, die Zellen mit nützlichen Befehlen zu belegen. Lassen Sie das schlechte Team den Code 0x00, das Team bye - 0x01 und die '0' den Code 0x02 haben, sobald er bereits geschrieben ist. Das schlechte Team wird vorerst dasselbe tun wie auf Wiedersehen, nur mit einem anderen Abschlusscode und Text (ich werde es in den Spoiler legen, fast genauso wie auf Wiedersehen):

bcmd_bad
 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   
Zeichnen Sie nun eine Adresstabelle. Der Einfachheit halber platzieren wir acht in jeder Zeile, es gibt 16 Zeilen. Die Tabelle ist ziemlich groß:

Byte-Befehlsadressentabelle
 bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad 
Wir schreiben den Hauptteil des Byte-Programms. Weisen Sie dazu Assembler-Variablen Befehlscodes zu. Wir werden folgende Vereinbarungen treffen:

  • Adressen zum Ausführen von Bytebefehlen beginnen mit bcmd_
  • Die Befehlscodes selbst werden in Variablen gespeichert, die mit b_ beginnen

Der Hauptteil des Byte-Programms sieht also folgendermaßen aus:

 start: .byte b_bye 

Deklarieren Sie die Größe des Datenstapels als stack_size. Lassen Sie es so weit sein 1024. Bei der Initialisierung werden wir RBP = RSP - stack_size ausführen.

Eigentlich bekommen wir einen solchen Programmcode (viertens)
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_bye .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_num0 = 0x02 bcmd_num0: push 0 jmp _next 


Kompilieren, ausführen:

$ as fort.asm -o fort.o -g -ahlsm >list.txt
$ ld forth.o -o forth
$ ./forth
bye!

Es funktioniert! Unser erstes Bytecode-Programm von einem Byte wurde gestartet :)
Dies wird natürlich so sein, wenn alles richtig gemacht wird. Und wenn nicht, ist das Ergebnis wahrscheinlich folgendes:

$ ./forth


Natürlich sind auch andere Optionen möglich, aber ich bin am häufigsten darauf gestoßen. Und wir brauchen einen Debugger.

Debugger Lyrics
Wie bereits erwähnt, habe ich GDB verwendet. Dies ist ein ziemlich leistungsfähiger Debugger, aber mit einer Befehlszeilenschnittstelle. Das Ausführen ist sehr einfach:

 $ gdb ./forth GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 Copyright (C) 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./forth...done. (gdb) 

Als nächstes debuggen wir durch Eingabe von Befehlen. Ich hatte genug Stunden, um einige notwendige Befehle zu finden und zu lernen, wie man sie zum Debuggen verwendet. Hier sind sie:
b <Label> - Setzen Sie einen Haltepunkt
l <label> - Quellcode anzeigen
r - Programm starten oder neu starten
Der Prozessor registriert den Status
s - Schritt

Denken Sie übrigens daran, dass Sie das Programm mit dem Schalter -g kompilieren müssen? Andernfalls sind Tags und Quellcode nicht verfügbar. In diesem Fall ist es möglich, nur durch disassemblierten Code zu debuggen und die Adressen im Speicher zu verwenden. Wir sind natürlich Inder, aber nicht in gleichem Maße ...

Aber irgendwie macht das Programm sehr wenig. Wir sagen nur "Hallo" zu ihr und sie sagt sofort "Tschüss!". Machen wir die echte "Hallo Welt!" auf Bytecode. Legen Sie dazu die Adresse und die Zeichenfolgenlänge auf den Stapel, führen Sie den Befehl aus, der die Zeichenfolge anzeigt, und anschließend den Befehl bye. Dazu sind neue Befehle erforderlich: Geben Sie ein, um die Zeichenfolge auszugeben, und leuchten Sie, um die Adresse und Länge der Zeichenfolge anzugeben. Zuerst schreiben wir Typ, lassen Sie seinen Code 0x80 sein. Wir brauchen wieder diesen Code mit dem Aufruf sys_write:

 b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 

Hier nehmen wir die Adresse und die Stringlänge mit POP-Befehlen aus dem Datenstapel. Das Aufrufen von int 0x80 kann das Register von R8 ändern, daher speichern wir es. Wir haben dies vorher nicht getan, weil das Programm beendet wurde. Der Inhalt dieser Register war egal. Dies ist nun ein regulärer Bytebefehl, nach dem der Bytecode weiterhin ausgeführt wird und Sie sich verhalten müssen.

Jetzt schreiben wir das Licht. Dies wird unser erstes Team mit Parametern sein. Nach dem Byte mit dem Code für diesen Befehl befinden sich Bytes mit der Nummer, die auf den Stapel gelegt wird. Es stellt sich sofort die Frage, welche Bittiefe hier benötigt wird. Um eine beliebige Zahl einzugeben, benötigen Sie 64 Bit. Aber jedes Mal, wenn der Befehl 9 Bytes belegt, was würde eine Zahl setzen? Wir verlieren also die Kompaktheit, eine der Haupteigenschaften des Bytecodes, und auch den Fort-Code ...

Die Lösung ist einfach: Wir werden mehrere Befehle für unterschiedliche Bittiefen ausführen. Dies sind lit8, lit16, lit32 und lit64. Für kleine Zahlen verwenden wir lit8 und lit16, für größere Zahlen lit32 und lit64. Am häufigsten werden kleine Zahlen verwendet, und für sie gibt es den kürzesten Befehl, der zwei Bytes benötigt. Nicht schlecht! .. Wir werden die Codes dieser Befehle 0x08 - 0x0B machen.

 b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next 

Hier verwenden wir den MOVSX-Befehl - dies ist eine ikonische Version des uns bereits bekannten MOVZX-Befehls. R8 haben wir einen Bytebefehlszähler. Wir laden den Wert der gewünschten Größe darauf, verschieben ihn zum nächsten Befehl und setzen den in 64 Bit konvertierten Wert auf den Stapel.

Vergessen Sie nicht, die Adressen neuer Teams in der Tabelle zu den gewünschten Positionen hinzuzufügen.

Das ist alles bereit, um dein erstes Programm "Hallo Welt!" Zu schreiben. auf unserem Bytecode. Lass uns mit dem Compiler arbeiten! :) :)

 start: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_bye 

Wir verwenden zwei verschiedene lit-Befehle: lit64, mit dem die Adresse der Zeichenfolge auf den Stapel gelegt wird, und lit8, mit dem die Länge auf den Stapel gelegt wird. Als nächstes führen wir zwei weitere Bytebefehle aus: type und bye.
Kompilieren, ausführen:

 $ as fort.asm -o fort.o -g -ahlsm >list.txt $ ld forth.o -o forth $ ./forth Hello, world! bye! 

Wir haben unseren Bytecode verdient! Dies ist das Ergebnis, das sein sollte, wenn alles in Ordnung ist.

Vollständige Quelle
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00 .quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x10 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_bye .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_num0 = 0x02 bcmd_num0: push 0 jmp _next b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 


Aber die Möglichkeiten sind immer noch sehr primitiv, man kann keine Bedingung, keinen Zyklus machen.

Wie ist es unmöglich? Sie können, alles ist in unseren Händen! Lassen Sie uns diese Zeile 10 Mal in der Schleife machen. Dies erfordert einen bedingten Verzweigungsbefehl sowie ein wenig Stapelarithmetik: einen Befehl, der den Wert auf dem Stapel um 1 verringert (bei Fort „1-“), und einen Vertex-Duplizierungsbefehl („dup“).

Mit Arithmetik ist alles einfach, ich werde nicht einmal kommentieren:

 b_dup = 0x18 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next 

Nun ein bedingter Sprung. Lassen Sie uns zunächst die Aufgabe vereinfachen - einen bedingungslosen Übergang. Es ist klar, dass Sie nur den Wert des Registers R8 ändern müssen. Das erste, was mir in den Sinn kommt, ist ein Bytebefehl, gefolgt von einem Parameter - die Übergangsadresse ist 64 Bit. Wieder neun Bytes. Benötigen wir diese neun Bytes? Übergänge treten normalerweise über kurze Entfernungen auf, oft innerhalb weniger hundert Bytes. Wir werden also nicht die Adresse verwenden, sondern den Offset!

Bittiefe? In vielen Fällen reichen 8 Bits (127 vorwärts / rückwärts) aus, aber manchmal reicht dies nicht aus. Daher machen wir dasselbe wie mit dem beleuchteten Befehl, wir machen zwei Optionen - 8 und 16 Ziffern, die Befehlscodes sind 0x10 und 0x11:

 b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next 
Jetzt ist der bedingte Übergang einfach zu implementieren. Wenn der Stapel 0 ist, gehen Sie zu _next und wenn nicht, gehen Sie zum Verzweigungsbefehl!
 b_qbranch8 = 0x12 bcmd_qbranch8: pop rax or rax, rax jnz bcmd_branch8 inc r8 jmp _next b_qbranch16 = 0x13 bcmd_qbranch16: pop rax or rax, rax jnz bcmd_branch16 add r8, 2 jmp _next 
Jetzt haben wir alles, um eine Schleife zu machen:
 start: .byte b_lit8 .byte 10 #  #  m0: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye 

Die ersten beiden Befehle - wir legen den Schleifenzähler auf den Stapel. Drucken Sie als Nächstes die Zeichenfolge Hallo. Dann subtrahieren wir 1 vom Zähler, duplizieren ihn und führen den Übergang durch (oder führen ihn nicht aus). Der Duplizierungsbefehl wird benötigt, da der bedingte Verzweigungsbefehl den Wert vom oberen Rand des Stapels übernimmt. Der Übergang ist hier acht Bit, da der Abstand nur wenige Bytes beträgt.

Wir fügen die Adressen neuer Befehle in eine Tabelle ein, kompilieren und führen sie aus.

Ich werde es in einen Spoiler stecken, sonst ist unser Programm ausführlich geworden.)
 $ as fort.asm -o fort.o -g -ahlsm >list.txt $ ld forth.o -o forth $ ./forth Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! bye! 


Nun, wir können schon Bedingungen und Zyklen machen!

Vollständige Quelle
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00 .quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_branch8, bcmd_branch16, bcmd_qbranch8, bcmd_qbranch16, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x10 .quad bcmd_dup, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_wm, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_lit8 .byte 10 #  #  m0: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_num0 = 0x02 bcmd_num0: push 0 jmp _next b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next b_dup = 0x18 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next b_qbranch8 = 0x12 bcmd_qbranch8: pop rax or rax, rax jnz bcmd_branch8 inc r8 jmp _next b_qbranch16 = 0x13 bcmd_qbranch16: pop rax or rax, rax jnz bcmd_branch16 add r8, 2 jmp _next 

Aber bis der fertigen Byte-Maschine fehlt eine weitere sehr wichtige Funktion. Wir können keinen anderen aus dem Bytecode aufrufen. Wir haben keine sogenannten Routinen, Prozeduren usw. Und in der Festung können wir ohne dies keine anderen Wörter als Kernelwörter in einigen Wörtern verwenden.

Wir bringen die Arbeit zum Ende. Hier brauchen wir zum ersten Mal einen Stapel Retouren. Es werden zwei Befehle benötigt - der Aufrufbefehl und der Rückgabebefehl (Aufruf und Beenden).

Der Aufrufbefehl macht im Prinzip dasselbe wie die Verzweigung - überträgt die Steuerung auf einen anderen Bytecode. Im Gegensatz zur Verzweigung müssen Sie die Rücksprungadresse jedoch im Rückgabestapel speichern, damit Sie zurückkehren und die Ausführung fortsetzen können. Es gibt noch einen weiteren Unterschied: Solche Anrufe können in viel größeren Entfernungen erfolgen. Daher führen wir den Aufrufbefehl in der Art einer Verzweigung aus, jedoch in drei Versionen - 8, 16 und 32 Bit.

 b_call8 = 0x0C bcmd_call8: movsx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 add r8, rax jmp _next b_call16 = 0x0D bcmd_call16: movsx rax, word ptr [r8] sub rbp, 8 add r8, 2 mov [rbp], r8 add r8, rax jmp _next b_call32 = 0x0E bcmd_call32: movsx rax, dword ptr [r8] sub rbp, 8 add r8, 4 mov [rbp], r8 add r8, rax jmp _next 

Wie Sie sehen können, werden hier im Gegensatz zu Übergängen 3 Teams hinzugefügt. Einer von ihnen ordnet R8 zum nächsten Bytebefehl um, und die verbleibenden zwei speichern den empfangenen Wert im Rückgabestapel. Übrigens habe ich hier versucht, die Prozessoranweisungen nicht nebeneinander zu setzen, damit der Prozessorförderer die Befehle parallel ausführen kann. Aber ich weiß nicht, wie sehr sich dies auswirkt. Falls gewünscht, können Sie die Tests überprüfen.

Es ist zu beachten, dass die Bildung eines Arguments für den Aufrufbefehl etwas anders ist als für die Verzweigung. Für die Verzweigung wird der Versatz als Differenz zwischen der Verzweigungsadresse und der Adresse des Bytes nach dem Bytebefehl berechnet. Und für den Aufrufbefehl ist dies der Unterschied zwischen der Sprungadresse und der Adresse des nächsten Befehls. Warum wird das benötigt?Dies führt zu weniger Prozessoranweisungen.

Nun der Rückgabebefehl. Tatsächlich besteht ihre Aufgabe nur darin, R8 vom Rückgabestapel wiederherzustellen und die Steuerung weiter auf die Byte-Maschine zu übertragen:

 b_exit = 0x1F bcmd_exit: mov r8, [rbp] add rbp, 8 jmp _next 

Diese Befehle werden sehr oft verwendet und müssen maximal optimiert werden. Der Befehl zum Beenden des Bytes belegt drei Maschinenbefehle. Kann man hier etwas reduzieren? Es stellt sich heraus, dass Sie können! Sie können den Übergangsbefehl einfach entfernen :)

Platzieren Sie ihn dazu über dem Einstiegspunkt der _next-Byte-Maschine:

 b_exit = 0x1F bcmd_exit: mov r8, [rbp] add rbp, 8 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] 

Übrigens müssen die wichtigsten und am häufigsten verwendeten Befehle (z. B. Aufruf) näher an der Byte-Maschine platziert werden, damit der Compiler einen Kurzsprungbefehl bilden kann. Dies ist in der Auflistung deutlich sichtbar. Hier ist ein Beispiel.

  262 0084 490FBE00 bcmd_lit8: movsx rax, byte ptr [r8] 263 0088 49FFC0 inc r8 264 008b 50 push rax 265 008c EB90 jmp _next 266 267 b_lit16 = 0x09 268 008e 490FBF00 bcmd_lit16: movsx rax, word ptr [r8] 269 0092 4983C002 add r8, 2 270 0096 50 push rax 271 0097 EB85 jmp _next 272 273 b_lit32 = 0x0A 274 0099 496300 bcmd_lit32: movsx rax, dword ptr [r8] 275 009c 4983C004 add r8, 4 276 00a0 50 push rax 277 00a1 E978FFFF jmp _next 277 FF 278 

In Zeile 265 und 271 benötigt der Befehl jmp jeweils 2 Byte, und in Zeile 277 ist derselbe Befehl bereits zu 5 Byte kompiliert, da die Sprungdistanz die Länge des Kurzbefehls überschreitet.

Daher werden Bytebefehle wie bad, bye, type weiter neu angeordnet, und wie call, branch, lit sind näher. Leider kann nicht viel in einen 127-Byte-Übergang passen.
Wir fügen der Tabelle der Befehlsadressen neue Befehle entsprechend ihren Codes hinzu.

Wir haben jetzt eine Herausforderung und eine Rückkehr, wir werden sie testen! Wählen Sie dazu den Zeilendruck in einem separaten Verfahren aus, und wir rufen ihn zweimal in einer Schleife auf. Und die Anzahl der Wiederholungen des Zyklus wird auf drei reduziert.

 start: .byte b_lit8 .byte 3 #  #  m0: .byte b_call16 .word sub_hello - . - 2 .byte b_call16 .word sub_hello - . - 2 .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye sub_hello: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_exit 

Call8 könnte hier verwendet werden, aber ich habe mich entschieden, call16 als wahrscheinlichste zu verwenden. Der Wert 2 wird aufgrund der Besonderheiten der Berechnung der Adresse für den Aufrufbytebefehl, über den ich geschrieben habe, abgezogen. Für call8 wird hier 1 abgezogen, für call32 jeweils 4. Wir
kompilieren und rufen auf:

 $ as forth.asm -o forth.o -g -ahlsm>list.txt $ ld forth.o -o forth $ ./forth Hello, world! Bad byte code! 

Ups ... wie sie sagen, etwas ist schief gelaufen :) Nun, wir starten GDB und sehen, was dort passiert. Ich habe sofort einen Haltepunkt auf bcmd_exit gesetzt, da klar ist, dass der Aufruf von sub_hello übergeben wird und der Hauptteil der Prozedur ausgeführt wird ... gestartet ... und das Programm den Haltepunkt nicht erreicht hat. Sofort bestand der Verdacht auf einen Byte-Befehlscode. Und tatsächlich lag der Grund in ihm. b_exit Ich habe den Wert 0x1f zugewiesen und die Adresse selbst wurde in die Tabellenzellennummer 0x17 eingefügt. Nun, dann werde ich den Wert von b_exit auf 0x17 korrigieren und es erneut versuchen:

 $ as forth.asm -o forth.o -g -ahlsm>list.txt $ ld forth.o -o forth $ ./forth Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! bye! 

Genau sechsmal Begrüßung und einmal auf Wiedersehen. Wie es sein sollte :)

Vollständige Quelle
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00 .quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_call8, bcmd_call16, bcmd_call32, bcmd_bad .quad bcmd_branch8, bcmd_branch16, bcmd_qbranch8, bcmd_qbranch16, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_exit # 0x10 .quad bcmd_dup, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_wm, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_lit8 .byte 3 #  #  m0: .byte b_call16 .word sub_hello - . - 2 .byte b_call16 .word sub_hello - . - 2 .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye sub_hello: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_exit .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next b_exit = 0x17 bcmd_exit: mov r8, [rbp] add rbp, 8 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_num0 = 0x02 bcmd_num0: push 0 jmp _next b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_call8 = 0x0C bcmd_call8: movsx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 add r8, rax jmp _next b_call16 = 0x0D bcmd_call16: movsx rax, word ptr [r8] sub rbp, 8 add r8, 2 mov [rbp], r8 add r8, rax jmp _next b_call32 = 0x0E bcmd_call32: movsx rax, dword ptr [r8] sub rbp, 8 add r8, 4 mov [rbp], r8 add r8, rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next b_dup = 0x18 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next b_qbranch8 = 0x12 bcmd_qbranch8: pop rax or rax, rax jnz bcmd_branch8 inc r8 jmp _next b_qbranch16 = 0x13 bcmd_qbranch16: pop rax or rax, rax jnz bcmd_branch16 add r8, 2 jmp _next b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 


Was ist das Ergebnis?


Wir haben eine vollständige und ziemlich schnelle 64-Bit-Stack-Byte-Maschine getestet. In Bezug auf die Geschwindigkeit ist diese Byte-Maschine möglicherweise eine der schnellsten ihrer Klasse (eine Stack-Byte-Maschine ohne JIT). Sie weiß, wie man Befehle nacheinander ausführt, bedingte und bedingungslose Sprünge ausführt, Prozeduren aufruft und von ihnen zurückkehrt. Gleichzeitig ist der verwendete Bytecode relativ kompakt. Grundsätzlich belegen Bytebefehle 1-3 Bytes, mehr ist sehr selten (nur große Zahlen und sehr weit entfernte Prozeduraufrufe). Es wird auch ein kleiner Satz von Bytebefehlen skizziert, der leicht zu erweitern ist. Angenommen, alle grundlegenden Befehle für die Arbeit mit dem Stapel (Drop, Swap, Over, Root usw. können in 20 Minuten geschrieben werden, der gleiche Betrag wird für arithmetische Ganzzahlbefehle verwendet).

Ein weiterer wichtiger Punkt. Der Bytecode enthält im Gegensatz zum klassischen direkt genähten Fort-Code keine Maschinenanweisungen, sodass er ohne Neukompilierung auf eine andere Plattform übertragen werden kann. Es reicht aus, den Kernel einmal in das Befehlssystem des neuen Prozessors umzuschreiben, und dies kann sehr schnell erfolgen.

Die aktuelle Version des Byte-Computers ist nicht spezifisch für eine bestimmte Sprache. Aber ich möchte die Fort-Sprache darauf implementieren, weil ich Erfahrung damit habe und der Compiler dafür sehr schnell fertig sein kann.

Wenn dies auf der Grundlage dieser Maschine Interesse besteht, werde ich im nächsten Artikel Zeichenfolgen und Zahlen, ein Fort-Wörterbuch und einen Interpreter eingeben und ausgeben. Sie können das Team mit Ihren Händen "berühren". Nun, im dritten Artikel werden wir einen Compiler erstellen und ein fast vollständiges Fort-System erhalten. Dann ist es möglich, einige Standardalgorithmen zu schreiben und zu kompilieren und die Leistung mit anderen Sprachen und Systemen zu vergleichen. Sie können zum Beispiel das Sieb von Eratosthenes und dergleichen verwenden.

Es ist interessant, mit Optionen zu experimentieren. Erstellen Sie beispielsweise die Befehlstabelle 16-Bit, und sehen Sie, wie sich dies auf die Leistung auswirkt. Sie können den Einstiegspunkt _next auch in ein Makro verwandeln. In diesem Fall vergrößert sich der Maschinencode jedes Bytebefehls um zwei Befehle (abzüglich des Übergangs und plus drei Befehle von _next). Das heißt, am Ende gibt es keinen Übergang zu _next, sondern den Inhalt des _next-Punkts selbst (dies sind 14 Bytes). Es ist interessant zu wissen, wie sich dies auf die Leistung auswirkt. Sie können auch versuchen, die Optimierung mithilfe von Registern durchzuführen. Beispielsweise speichert eine Standardschleife mit einem Zähler in der Festung den Zähler auf dem Rückgabestapel. Sie können eine Registerversion erstellen und auch testen.

Sie können auch einen Compiler aus Ausdrücken erstellen, die in der klassischen Form geschrieben sind (z. B. A = 5 + (B + C * 4)).

Generell gibt es Raum zum Experimentieren! :) :)

Fortsetzung: Byte-Maschine für das Fort (und nicht nur) in Native American (Teil 2)

Source: https://habr.com/ru/post/de431932/


All Articles