Fort Byte Car (et plus) Native American

image

Oui, oui, c'est "l'octet" et c'est en indien (pas indien). Je vais commencer dans l'ordre. Récemment ici, sur Habré, des articles sur le bytecode ont commencé à apparaître. Et il était une fois, je me suis amusé à écrire des systèmes Fort. Bien sûr, en assembleur. Ils étaient en 16 bits. Je n'ai jamais programmé sur x86-64. Même avec 32 ne pouvait pas jouer. Alors la pensée est venue - pourquoi pas? Pourquoi ne pas remuer le fort 64 bits, et même avec le bytecode? Oui, et sous Linux, où je n'ai pas non plus écrit de système.

J'ai un serveur domestique avec Linux. En général, j'ai googlé un peu et découvert que l'assembleur sous Linux s'appelle GAS et la commande as. Je me connecte via SSH au serveur, en tapant - oui! Je l'ai déjà installé. Vous avez toujours besoin d'un éditeur de liens, tapez ld - oui! Alors, et essayez d'écrire quelque chose d'intéressant dans l'assembleur. Sans civilisation, seulement une forêt, comme de vrais Indiens :) Sans environnement de développement, seulement une ligne de commande et Midnight Commander. L'éditeur sera Nano, qui se bloque sur mon F4 en mc. Comment chante le groupe «Zero»? Un vrai Indien n'a besoin que d'une chose ... De quoi d'autre un vrai Indien a-t-il besoin? Bien sûr, un débogueur. Nous tapons gdb - est! Eh bien, appuyez sur Maj + F4, et c'est parti!

L'architecture


Pour commencer, décidons de l'architecture. Avec des profondeurs de bits déjà déterminées, 64 bits. Dans les implémentations Fort classiques, les segments de données et de code sont identiques. Mais nous essaierons de le faire correctement. Nous aurons seulement le code dans le segment de code, les données dans le segment de données. En conséquence, nous obtenons un noyau pour la plate-forme et un code d'octet complètement indépendant de la plate-forme.

Essayons de créer la machine à octets empilés la plus rapide (mais sans JIT). Ainsi, nous aurons une table contenant 256 adresses - une pour chaque commande d'octets. Moins que tout - une vérification supplémentaire, ce sont 1-2 instructions du processeur. Et nous avons besoin rapidement, sans compromis.

Piles


Habituellement, dans les implémentations Fort, la pile de retour du processeur (* SP) est utilisée comme une pile de données et la pile de retours du système fort est implémentée en utilisant d'autres moyens. En effet, notre machine sera empilée, et le travail principal est sur la pile de données. Par conséquent, faisons de même - RSP sera une pile de données. Eh bien, laissez la pile de retour être RBP, qui, par défaut, fonctionne également avec le segment de pile. Ainsi, nous aurons trois segments de mémoire: un segment de code, un segment de données et un segment de pile (il aura à la fois une pile de données et une pile de retour).

Registres


J'entre dans la description des registres x86-64, et oups! Il y a jusqu'à 8 registres à usage général supplémentaires (R8 - R16), par rapport aux modes 32 ou 16 bits.

Déjà décidé qu'ils auront besoin de RSP et de RBP. Encore besoin d'un pointeur (compteur) des commandes de bytecode. Des opérations sur ce registre, seule la lecture en mémoire est nécessaire. Les registres principaux (RAX, RBX, RCX, RDX, RSI, RDI) sont plus flexibles, universels, avec eux il y a beaucoup de commandes spéciales. Ils nous seront utiles pour diverses tâches, et pour le compteur d'instructions bytecode nous prenons l'un des nouveaux registres pour moi, que ce soit R8.

Commençons


Je n'ai aucune expérience en programmation sous Linux en langage assembleur. Par conséquent, pour commencer, nous trouverons le "Bonjour tout le monde" pour comprendre comment le programme démarre et affiche le texte. De façon inattendue pour moi, j'ai trouvé des options avec une étrange syntaxe où même la source et le récepteur sont réorganisés. Il s'est avéré que c'est la syntaxe AT&T, et elle est principalement écrite sous GAS dessus. Mais une autre option de syntaxe est prise en charge, elle s'appelle la syntaxe Intel. En réfléchissant, j'ai décidé de l'utiliser tout de même. Eh bien, écrivez au début de .intel_syntax noprefix.

Compilez et exécutez «Hello, world» pour vous assurer que tout fonctionne. En lisant l'aide et les expériences, j'ai commencé à utiliser la commande suivante pour compiler:
$ as fort.asm -o fort.o -g -ahlsm >list.txt
Ici, le commutateur -o indique le fichier de résultats, le commutateur -g indique de générer des informations de débogage et le commutateur -ahlsm définit le format de liste. Et je garde la sortie dans la liste, vous y voyez beaucoup de choses utiles. J'avoue qu'au début du travail, je n'ai pas fait la liste et je n'ai même pas spécifié le commutateur -g. J'ai commencé à utiliser le commutateur -g après la première utilisation du débogueur et j'ai commencé à faire la liste après l'apparition des macros dans le code :)

Après cela, nous utilisons l'éditeur de liens, mais voici rien de plus simple:

$ ld forth.o -o forth
Eh bien, cours!
$ ./forth
Hello, world!

Ça marche.

Ce fut le premier quatrième.asm (en fait c'est «Hellow, world!», Bien sûr)
 .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 #  № 1 — stdout mov ecx, OFFSET FLAT:msg #     mov edx, len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit xor ebx, ebx #    0 int 0x80 #   

Soit dit en passant, j'ai découvert plus tard qu'en x86-64, il est plus correct d'utiliser syscall pour un appel système, plutôt que int 0x80. L'appel 0x80 est considéré comme obsolète pour cette architecture, bien qu'il soit pris en charge.

Un début a été fait, et maintenant ...

C'est parti!


Qu'il y ait au moins quelques détails, nous allons écrire le code d'une commande d'un octet. Que ce soit le mot Fort "0", en mettant 0 en haut de la pile:

 bcmd_num0: push 0 jmp _next 

Au moment où cette commande est exécutée, R8 pointe déjà vers la commande d'octet suivante. Il est nécessaire de le lire, d'augmenter R8, de déterminer l'adresse exécutable par le code de la commande d'octets, et de lui transférer le contrôle.

Mais ... quelle profondeur de bits sera la table d'adresses de commande d'octets? Ensuite, j'ai dû fouiller dans le nouveau système de commande x86-64 pour moi. Hélas, je n'ai pas trouvé de commandes qui vous permettent d'aller à l'offset en mémoire. Donc, calculez l'adresse ou l'adresse sera prête - 64 bits. Nous n'avons pas le temps de calculer, ce qui signifie - 64 bits. Dans ce cas, la taille de la table sera de 256 * 8 = 4096 octets. Enfin, encodez l'appel _next:

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

Pas mal, il me semble ... Il n'y a que trois instructions processeur, lors du passage d'une commande d'octet Ă  une autre.

En fait, ces commandes n'étaient pas si faciles pour moi. J'ai dû plonger à nouveau dans le système de commande 0x86-64 et trouver une nouvelle commande MOVZX pour moi. En fait, cette commande convertit une valeur de 8, 16 ou 32 bits en un registre 64 bits. Il existe deux variantes de cette commande: unsigned, où les chiffres supérieurs sont remplis de zéros et celui signé est MOVSX. Dans la version signée, le signe se développe, c'est-à-dire que pour les nombres positifs, les zéros iront aux chiffres supérieurs et pour les négatifs, les uns. Cette option nous est également utile pour la commande lit byte.

Au fait, cette option est-elle la plus rapide? Peut-être que quelqu'un suggérera encore plus vite?

Eh bien, nous avons maintenant une machine d'octets qui peut exécuter une séquence de commandes d'octets et les exécuter. Il faut le tester en pratique, pour forcer à exécuter au moins une équipe. Mais lequel? Zéro sur la pile? Mais ici, vous ne connaissez même pas le résultat, si vous ne regardez pas la pile sous le débogueur ... Mais si le programme a commencé, il peut être terminé :)

Nous écrivons une commande bye qui termine le programme et écrit à ce sujet, d'autant plus que nous avons «Hellow, world!».

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

La seule chose qui reste est de créer une table d'adresses de commande d'octets, d'initialiser les registres et de démarrer la machine d'octets. Donc ... il y a 256 valeurs dans le tableau, et il y a deux commandes. Qu'y a-t-il dans les autres cellules?
Le reste aura un code d'opération invalide. Mais, vous ne pouvez pas le vérifier, ce sont des équipes supplémentaires, nous en avons trois maintenant, et avec le contrôle ce sera cinq. Donc, nous allons faire une telle commande stub - une mauvaise équipe. D'abord, nous remplissons tout le tableau pour cela, puis nous commençons à occuper les cellules avec des commandes utiles. Laissez la mauvaise équipe avoir le code 0x00, l'équipe bye - 0x01 et le '0' aura le code 0x02, une fois qu'il est déjà écrit. La mauvaise équipe pour l'instant fera la même chose que bye, uniquement avec un code et un texte d'achèvement différents (je le mettrai dans le spoiler, presque comme Bye):

bcmd_bad
 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout 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 #   
Dessinez maintenant un tableau d'adresses. Pour plus de commodité, nous en placerons huit dans chaque ligne, il y en aura 16. La table est assez grande:

Table d'adresses de commande d'octets
 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 
Nous Ă©crivons le corps du programme d'octets. Pour ce faire, affectez des codes de commande aux variables assembleur. Nous aurons les accords suivants:

  • Les adresses pour l'exĂ©cution des commandes d'octets commenceront sur bcmd_
  • Les codes de commande eux-mĂŞmes seront stockĂ©s dans des variables commençant par b_

Ainsi, le corps du programme d'octets sera comme ceci:

 start: .byte b_bye 

Déclarez la taille de la pile de données en tant que stack_size. Soit jusqu'à présent 1024. Lors de l'initialisation, nous ferons RBP = RSP - stack_size.

En fait, nous obtenons un tel code de programme (coming.asm)
 .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 #  № 1 — stdout 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 #  № 1 — stdout 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 


Compiler, exécuter:

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

Ça marche! Notre premier programme de bytecode à partir d'un octet a été lancé :)
Bien sûr, ce sera le cas si tout est fait correctement. Et sinon, le résultat est probablement le suivant:

$ ./forth


Bien sûr, d'autres options sont possibles, mais je les ai rencontrées le plus souvent. Et nous avons besoin d'un débogueur.

Paroles de Debugger
Comme déjà mentionné, j'ai utilisé GDB. Il s'agit d'un débogueur assez puissant, mais avec une interface de ligne de commande. L'exécuter est très simple:

 $ 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) 

Ensuite, en entrant des commandes, nous déboguons. J'ai eu suffisamment d'heures pour trouver certaines commandes nécessaires et apprendre à les utiliser pour le débogage. Les voici:
b <label> - définir un point d'arrêt
l <label> - afficher le code source
r - démarrer ou redémarrer le programme
ir - afficher le statut des registres du processeur
s - Ă©tape

Soit dit en passant, rappelez-vous que vous devez compiler le programme avec le commutateur -g? Sinon, les balises et le code source ne seront pas disponibles. Dans ce cas, il ne sera possible de déboguer que par du code désassemblé et d'utiliser les adresses en mémoire. Nous sommes, bien sûr, des Indiens, mais pas dans la même mesure ...

Mais en quelque sorte, le programme fait très peu. Nous lui disons seulement «Bonjour» et elle dit immédiatement «Au revoir!». Faisons le vrai "Bonjour tout le monde!" sur bytecode. Pour ce faire, placez l'adresse et la longueur de chaîne sur la pile, puis exécutez la commande qui affiche la chaîne, puis la commande bye. Pour faire tout cela, de nouvelles commandes sont nécessaires: tapez pour sortir la chaîne, et allumée pour mettre l'adresse et la longueur de la chaîne. D'abord, nous écrivons le type, que son code soit 0x80. Nous avons encore besoin de ce morceau de code avec l'appel sys_write:

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

Ici, nous prenons l'adresse et la longueur de chaîne de la pile de données à l'aide des commandes POP. Appeler int 0x80 peut changer le registre de R8, nous le sauvegardons donc. Nous ne l'avons pas fait auparavant parce que le programme se terminait. Le contenu de ces registres s'en fichait. Il s'agit maintenant d'une commande d'octet standard, après quoi le code d'octet continue d'être exécuté, et vous devez vous comporter.

Maintenant écrivons le lit. Ce sera notre première équipe avec des paramètres. Après l'octet avec le code de cette commande, il y aura des octets contenant le numéro qu'il mettra sur la pile. La question se pose immédiatement - quelle profondeur de bits est nécessaire ici? Pour mettre n'importe quel nombre, vous avez besoin de 64 bits. Mais, chaque fois que la commande occupera 9 octets, qu'est-ce qui mettrait un chiffre? Nous perdons donc la compacité, l'une des principales propriétés du bytecode, et le code de fort aussi ...

La solution est simple - nous ferons plusieurs commandes pour différentes profondeurs de bits. Ceux-ci seront allumés8, allumés16, allumés32 et allumés64. Pour les petits nombres, nous utiliserons lit8 et lit16, pour les plus grands - lit32 et lit64. Les petits nombres sont le plus souvent utilisés, et pour eux, il y aura la commande la plus courte, qui prend deux octets. Pas mal! .. On va faire les codes de ces commandes 0x08 - 0x0B.

 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 

Ici, nous utilisons la commande MOVSX - il s'agit d'une version emblématique de la commande MOVZX que nous connaissons déjà. R8 nous avons un compteur de commande d'octets. Nous chargeons la valeur de la taille désirée dessus, la déplaçons à la commande suivante et plaçons la valeur convertie en 64 bits sur la pile.

N'oubliez pas d'ajouter les adresses des nouvelles équipes dans le tableau aux postes souhaités.

C'est tout prĂŞt pour Ă©crire votre premier programme "Bonjour, monde!" sur notre bytecode. Travaillons avec le compilateur! :)

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

Nous utilisons deux commandes allumées différentes: lit64 pour mettre l'adresse de la chaîne sur la pile, et lit8, avec lequel nous poussons la longueur sur la pile. Ensuite, nous exécutons deux commandes d'octets supplémentaires: type et bye.
Compiler, exécuter:

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

A gagné notre bytecode! C'est le résultat qui devrait être si tout est normal.

Source complète
 .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 #  № 1 — stdout 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 #  № 1 — stdout 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 #  № 1 — stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 


Mais les possibilités sont encore très primitives, on ne peut pas faire une condition, un cycle.

Comment ne pas le faire? Vous pouvez, tout est entre nos mains! Faisons 10 fois cette ligne dans la boucle. Cela nécessitera une commande de branchement conditionnel, ainsi qu'un peu d'arithmétique de pile: une commande diminuant la valeur sur la pile de 1 (sur fort "1-") et une commande de duplication de sommets ("dup").

Avec l'arithmétique, tout est simple, je ne commenterai même pas:

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

Maintenant, un saut conditionnel. Pour commencer, simplifions la tâche - une transition inconditionnelle. Il est clair qu'il suffit de modifier la valeur du registre R8. La première chose qui me vient à l'esprit est une commande d'octets, suivie d'un paramètre - l'adresse de transition est de 64 bits. Encore neuf octets. Avons-nous besoin de ces neuf octets? Les transitions se produisent généralement sur de courtes distances, souvent dans quelques centaines d'octets. Donc, nous n'utiliserons pas l'adresse, mais l'offset!

Profondeur de bits? Dans de nombreux cas, 8 bits (127 avant / arrière) suffiront, mais parfois ce ne sera pas suffisant. Par conséquent, nous ferons la même chose qu'avec la commande allumée, nous ferons deux options - 8 et 16 chiffres, les codes de commande seront 0x10 et 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 
La transition conditionnelle est désormais facile à mettre en œuvre. Si la pile vaut 0, passez à _next, et sinon, allez à la commande branch!
 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 
Maintenant, nous avons tout pour faire une boucle:
 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 

Les deux premières commandes - nous mettons le compteur de boucles sur la pile. Ensuite, imprimez la chaîne Hello. Ensuite, nous soustrayons 1 du compteur, le dupliquons et effectuons (ou n'effectuons pas) la transition. La commande de duplication est nécessaire car la commande de branche conditionnelle prend la valeur en haut de la pile. La transition ici est de huit bits, car la distance n'est que de quelques octets.

Nous mettons les adresses des nouvelles commandes dans une table, compilons et exécutons.

Je vais le mettre dans un spoiler, sinon notre programme est devenu verbeux)
 $ 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! 


Eh bien, nous pouvons déjà faire des conditions et des cycles!

Source complète
 .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 #  № 1 — stdout 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 #  № 1 — stdout 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 #  № 1 — stdout 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 

Mais jusqu'à ce que la machine d'octets terminée manque une autre fonction très importante. Nous ne pouvons pas appeler un autre à partir du bytecode. Nous n'avons pas ce qu'on appelle des routines, des procédures, etc. Et dans le fort, sans cela, nous ne pouvons pas utiliser des mots autres que les mots du noyau dans certains mots.

Nous terminons le travail. Ici, pour la première fois, nous avons besoin d'une pile de retours. Deux commandes sont nécessaires - la commande d'appel et la commande de retour (appel et sortie).

La commande call, en principe, fait la même chose que branch - transfère le contrôle à un autre morceau de bytecode. Mais, contrairement à branch, vous devez toujours enregistrer l'adresse de retour dans la pile de retour afin de pouvoir retourner et poursuivre l'exécution. Il y a une autre différence - de tels appels peuvent se produire à des distances beaucoup plus grandes. Par conséquent, nous faisons la commande d'appel à l'image de branche, mais en trois versions - 8, 16 et 32 ​​bits.

 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 

Comme vous pouvez le voir, ici, contrairement aux transitions, 3 équipes sont ajoutées. L'un d'eux réorganise R8 à la commande d'octet suivante, et les deux autres stockent la valeur reçue dans la pile de retour. Au fait, ici, j'ai essayé de ne pas mettre les instructions du processeur côte à côte, afin que le convoyeur du processeur puisse exécuter les commandes en parallèle. Mais je ne sais pas à quel point cela donne un effet. Si vous le souhaitez, vous pouvez vérifier les tests.

Il convient de garder à l'esprit que la formation d'un argument pour la commande d'appel est quelque peu différente de celle de la branche. Pour la branche, le décalage est calculé comme la différence entre l'adresse de branche et l'adresse de l'octet suivant la commande byte. Et pour la commande d'appel, c'est la différence entre l'adresse de saut et l'adresse de la commande suivante. Pourquoi est-ce nécessaire?Cela se traduit par moins d'instructions du processeur.

Maintenant, la commande de retour. En fait, son travail consiste uniquement à restaurer R8 à partir de la pile de retour et à transférer le contrôle vers la machine d'octets:

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

Ces commandes seront utilisées très souvent et doivent être optimisées au maximum. La commande exit byte occupe trois instructions machine. Est-il possible de réduire quelque chose ici? Il s'avère que vous le pouvez! Vous pouvez simplement supprimer la commande de transition :)

Pour ce faire, placez-la au-dessus du point d'entrée de la machine _next byte:

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

Soit dit en passant, les commandes les plus importantes et les plus fréquemment utilisées (par exemple, appel) doivent être placées plus près de la machine à octets afin que le compilateur puisse former une commande de saut court. Ceci est clairement visible dans la liste. Voici un exemple.

  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 

Ici, aux lignes 265 et 271, la commande jmp prend 2 octets chacune, et à la ligne 277, la même commande est déjà compilée à 5 octets, car la distance de saut a dépassé la longueur de la commande courte.

Par conséquent, les commandes d'octet telles que bad, bye, type sont réorganisées davantage, et telles que call, branch, lit sont plus proches. Malheureusement, il n'y a pas grand-chose qui puisse tenir dans une transition de 127 octets.
Nous ajoutons de nouvelles commandes Ă  la table des adresses de commandes en fonction de leurs codes.

Donc, nous avons maintenant un défi et un retour, nous allons les tester! Pour ce faire, sélectionnez l'impression de ligne dans une procédure distincte, et nous l'appellerons deux fois en boucle. Et le nombre de répétitions du cycle est réduit à trois.

 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 pourrait être utilisé ici, mais j'ai décidé d'utiliser call16 comme le plus probablement utilisé. La valeur 2 est soustraite en raison des particularités du calcul de l'adresse pour la commande d'octet d'appel sur laquelle j'ai écrit. Pour call8, 1 sera déduit ici, pour call32, respectivement, 4. Nous
compilons et appelons:

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

Oups ... comme on dit, quelque chose s'est mal passé :) Eh bien, nous lançons GDB et voyons ce qui s'y passe. J'ai défini un point d'arrêt immédiatement sur bcmd_exit, car il est clair que l'appel sub_hello passe, et le corps de la procédure est en cours d'exécution ... lancé ... et le programme n'a pas atteint le point d'arrêt. Immédiatement, il y avait une suspicion d'un code de commande octet. Et, en effet, la raison était en lui. b_exit J'ai attribué la valeur 0x1f, et l'adresse elle-même a été placée dans le numéro de cellule du tableau 0x17. Eh bien, je vais corriger la valeur de b_exit à 0x17 et réessayer:

 $ 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! 

Exactement six fois le salut et une fois au revoir. Comme il se doit :)

Source complète
 .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 #  № 1 — stdout 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 #  № 1 — stdout 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 #  № 1 — stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 


Quel est le résultat


Nous avons fait et testé une machine d'octets de pile 64 bits complète et assez rapide. En vitesse, cette machine à octets sera peut-être l'une des plus rapides de sa catégorie (une machine à octets empilés sans JIT). Elle sait comment exécuter des commandes séquentiellement, effectuer des sauts conditionnels et inconditionnels, appeler des procédures et en revenir. Dans le même temps, le bytecode utilisé est raisonnablement compact. Fondamentalement, les commandes d'octets prennent 1-3 octets, plus est très rare (seulement de grands nombres et des appels de procédure très éloignés). Un petit ensemble de commandes d'octets est également esquissé, ce qui est facile à développer. Supposons que toutes les commandes de base pour travailler avec la pile (drop, swap, over, root, etc. peuvent être écrites en 20 minutes, le même montant ira aux commandes arithmétiques entières).

Un autre point important. Le bytecode, contrairement au code de fort cousu direct classique, ne contient pas d'instructions machine, il peut donc être transféré sans recompilation sur une autre plate-forme. Il suffit de réécrire le noyau une fois dans le système d'instructions du nouveau processeur, et cela peut se faire très rapidement.

La version actuelle de la machine d'octets n'est pas spécifique à une langue particulière. Mais je veux faire l'implémentation du langage Fort dessus parce que j'ai de l'expérience avec lui, et le compilateur peut être fait très rapidement.

S'il y a un intérêt pour cela, basé sur cette machine, dans le prochain article, je ferai des entrées-sorties de chaînes et de nombres, un dictionnaire fort et un interprète. Vous pouvez "toucher" l'équipe avec vos mains. Eh bien, dans le troisième article, nous ferons un compilateur, et nous aurons un système de fortification presque complet. Ensuite, il sera possible d'écrire et de compiler des algorithmes standard et de comparer les performances avec d'autres langages et systèmes. Vous pouvez utiliser, par exemple, le tamis d'Eratosthène, et similaire.

Il est intéressant d'expérimenter des options. Par exemple, créez la table de commandes 16 bits et voyez comment cela affectera les performances. Vous pouvez également transformer le point d'entrée _next en macro, auquel cas le code machine de chaque commande d'octets augmentera de taille de deux commandes (moins la transition et plus trois commandes de _next). Autrement dit, à la fin, il n'y aura pas de transition vers _next, mais le contenu du point _next lui-même (c'est 14 octets). Il est intéressant de savoir comment cela affectera les performances. Vous pouvez également essayer de faire l'optimisation à l'aide de registres. Par exemple, une boucle standard avec un compteur dans le fort stocke le compteur sur la pile de retour. Vous pouvez créer une version de registre et également la tester.

Vous pouvez également créer un compilateur d'expressions écrites sous la forme classique (par exemple, A = 5 + (B + C * 4)).

En général, il y a de la place pour l'expérimentation! :)

Suite: Byte-machine pour le fort (et pas seulement) en amérindien (partie 2)

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


All Articles