Modèle de développement utilisant un processeur basé sur la pile comme exemple

Avez-vous déjà posé la question "comment fonctionne le processeur?". Oui, oui, exactement celui qui se trouve dans votre PC / ordinateur portable / smartphone. Dans cet article, je veux donner un exemple d'un processeur auto-inventé avec un design dans Verilog. Verilog n'est pas exactement le langage de programmation auquel il ressemble. Il s'agit du langage de description du matériel. Le code écrit n'est exécuté par rien (sauf si vous l'exécutez dans le simulateur, bien sûr), mais se transforme en conception du circuit physique, ou sous la forme perçue par FPGA (Field Programmable Gate Array).


Avertissement: cet article est le résultat d'un travail sur un projet à l'université, donc le temps de travail était limité et de nombreuses parties du projet n'en sont encore qu'au stade initial de développement.


Veuillez noter que le processeur créé dans cet article a peu de choses en commun avec les processeurs modernes et répandus, mais j'ai essayé d'atteindre un objectif légèrement différent avec sa création.


Pour vraiment comprendre le processus de programmation, vous devez imaginer comment fonctionne chacun des outils utilisés: un compilateur / interprète du langage, une machine virtuelle, le cas échéant, du code intermédiaire et, bien sûr, le processeur lui-même. Très souvent, les gens qui étudient la programmation en sont au premier stade depuis longtemps - ils ne pensent qu'à la façon dont le langage et son compilateur fonctionnent. Cela conduit souvent à des erreurs dont les solutions sont inconnues du programmeur débutant, car il n'a aucune idée d'où viennent les racines de ces problèmes. J'ai moi-même vu plusieurs exemples en direct où la situation était à peu près la même que dans la description ci-dessus, j'ai donc décidé d'essayer de résoudre cette situation et de créer un ensemble de choses qui aideront les débutants à comprendre toutes les étapes.


Ce kit comprend:


  • Langue réellement inventée
  • Mettre en surbrillance le plugin pour VS Code
  • Compilateur
  • Jeu d'instructions
  • Un processeur simple capable d'exécuter cet ensemble d'instructions (écrit en Verilog)

Je vous rappelle une fois de plus que cet article ne décrit rien de semblable à un vrai processeur moderne, il décrit un modèle facile à comprendre sans entrer dans les détails.


Choses dont vous aurez besoin si vous voulez le faire vous-même:


Pour exécuter une simulation de processeur, vous avez besoin de ModelSim, que vous pouvez télécharger sur le site Web d'Intel.


Pour exécuter le compilateur OurLang, vous avez besoin de la version Java> = 8.


Liens vers les projets:
https://github.com/IamMaxim/OurCPU
https://github.com/IamMaxim/OurLang


Extension:
https://github.com/IamMaxim/ourlang-vscode


Pour construire la partie Verilog, j'utilise généralement un script bash:


#/bin/bash vlib work vlog *.v vsim -c testbench_1 -do "run; exit" 

Mais cela peut être répété via l'interface graphique.


Il est pratique d'utiliser Intellij IDEA pour travailler avec le compilateur. L'essentiel est de garder une trace des modules du module dont vous avez besoin dans les dépendances. Je n'ai pas posté le .jar prêt à l'emploi en accès ouvert, car je m'attends à ce que le lecteur lise le code source du compilateur.


Les modules lancés sont le compilateur et l'interpréteur. Tout est clair avec le compilateur, Interpreter n'est qu'un simulateur de OurCPU en Java, mais nous ne le considérerons pas dans cet article.


Jeu d'instructions


Je pense qu'il vaut mieux commencer avec le jeu d'instructions.


Il existe plusieurs architectures de jeux d'instructions:


  • Basé sur la pile est ce qui est décrit dans l'article. Une caractéristique distinctive est que tous les opérandes sont poussés sur la pile et extraits de la pile, ce qui exclut immédiatement la possibilité de paralléliser l'exécution, mais c'est l'une des approches les plus simples pour travailler avec des données.
  • Basé sur l'accumulateur - le résultat est qu'il n'y a qu'un seul registre qui stocke une valeur qui est modifiée par des instructions.
  • Le registre est ce qui est utilisé dans les processeurs modernes, car il vous permet d'obtenir des performances maximales grâce à diverses optimisations, notamment la parallélisation de l'exécution, le pipelining, etc.

Notre jeu d'instructions de processeur contient 30 instructions


Ensuite, je propose de jeter un œil à l'implémentation du processeur:


Le code se compose de plusieurs modules:


  • CPU
  • RAM
  • Modules pour chaque instruction

La RAM est un module contenant directement la mémoire elle-même, ainsi qu'un moyen d'accéder aux données qu'elle contient.


CPU - un module qui contrôle directement la progression du programme: lit les instructions, transfère le contrôle à l'instruction souhaitée, stocke les registres nécessaires (pointeur sur l'instruction en cours, etc.).


Presque toutes les instructions ne fonctionnent qu'avec la pile, alors suivez-les. Certains (par exemple putw, putb, jmp et jif) ont un argument supplémentaire dans l'instruction elle-même. Ils doivent passer la totalité de l'instruction afin de pouvoir lire les données nécessaires.


Voici un aperçu général du fonctionnement du processeur:



Principes généraux de conception des appareils au niveau de l'instruction


Je pense qu'il est temps de se familiariser avec l'appareil directement des programmes eux-mêmes. Comme vous pouvez le voir sur le schéma ci-dessus, après chaque instruction, l'adresse passe à la suivante. Cela donne un cours linéaire au programme. Lorsqu'il devient nécessaire de rompre cette linéarité (condition, boucle, etc.), des instructions de branchement sont utilisées (dans notre jeu d'instructions, ce sont jmp et jif).


Lors de l'appel de fonctions, nous devons sauvegarder l'état actuel de tout, et pour cela, il existe des enregistrements d'activation - des enregistrements qui stockent ces informations. Ils ne sont liés en aucune façon au processeur ou aux instructions, c'est juste un concept que le compilateur utilise lors de la génération de code. L'enregistrement d'activation dans OurLang a la structure suivante:



Comme le montre ce diagramme, les variables locales sont également stockées dans l'enregistrement d'activation, ce qui vous permet de calculer l'adresse de la variable en mémoire au moment de la compilation plutôt qu'au moment de l'exécution, et accélère ainsi l'exécution du programme.


Pour les appels de fonction, notre jeu d'instructions fournit des méthodes pour travailler avec deux registres contenus dans le module CPU (pointeur d'opération et pointeur d'adresse d'activation) - putopa / popopa, putara / popara.


Compilateur


Voyons maintenant la partie la plus proche du programmeur final - le compilateur. En général, le compilateur en tant que programme se compose de 3 parties:


  • Lexer
  • Analyseur
  • Compilateur

Le lexer est responsable de la traduction du code source du programme en unités lexicales que l'analyseur comprend.


L'analyseur syntaxique construit un arbre de syntaxe abstraite à partir de ces unités lexicales.


Le compilateur parcourt cet arbre et génère une sorte de code composé d'instructions de bas niveau. Il peut s'agir soit d'un bytecode, soit d'un code binaire prêt à être exécuté par le processeur.


Dans le compilateur OurLang, ces parties sont représentées respectivement par des classes


  • Lexer.java
  • Parser.java
  • Compiler.java

La langue


OurLang est à ses balbutiements, c'est-à-dire qu'il fonctionne, mais jusqu'à présent, il n'y a pas beaucoup de choses en lui et même la partie centrale du langage n'a pas été finalisée. Mais pour comprendre l'essence du compilateur, l'état actuel suffit.


Comme exemple de programme de compréhension de la syntaxe, ce fragment de code est proposé (il est également utilisé pour tester la fonctionnalité):


 // single-line comments /* * Multi-line comments */ function print(int arg) { instr(putara, 0); instr(putw, 4); instr(add, 0); instr(lw, 0); instr(printword, 0); } function func1(int arg1, int arg2): int { print(arg1); print(arg2); if (arg1 == 0) { return arg2; } else { return func1(arg1 - 1, arg2); }; } function main() { var i: int; i = func1(1, 10); if (i == 0) { i = 1; } else { i = 2; }; print(i); } 

Je ne me concentrerai pas sur la langue, je la laisserai à votre étude. Grâce au code du compilateur, bien sûr;).


Lors de l'écriture, j'ai essayé de rendre le code explicite clair sans commentaire, donc il ne devrait y avoir aucun problème à comprendre le code du compilateur.


Eh bien, bien sûr, la chose la plus intéressante est d'écrire du code, puis de regarder ce qu'il se transforme. Heureusement, le compilateur OurLang génère du code de type assembleur avec des commentaires,
ce qui permet de ne pas se tromper sur ce qui se passe à l'intérieur.


Je recommande également d'installer l'extension pour Visual Studio Code, cela facilitera le travail avec la langue.


Bonne chance pour apprendre le projet!

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


All Articles