Simulateur de langage machine simple
Il s'agit d'un simulateur de langage machine simple qui gère les opérations de base telles que lire, écrire, charger, stocker, ajouter, soustraire, multiplier, diviser, module, branche, branche si négative, branche si zéro.
Les entrées sont obtenues de l'utilisateur en hexadécimal, la mémoire est simulée sous la forme arrayd'entiers intégrés pouvant contenir un maximum de 1 mot.
Un mot est composé de 4 chiffres, les deux premiers représentent le code d'opérande (code d'instruction sml), les deux derniers représentent l'opérande (emplacement en mémoire). Le simulateur lit et affiche également les chaînes littérales.
Voici le code.
constantes.h
constexpr unsigned read = 0xA; // Read a word(int) from the keyboard into a specific location in memory
constexpr unsigned write = 0xB; // Write a word(int) from a specific location in memory to the screen
constexpr unsigned read_str = 0xC; // Read a word(string) from the keyboard into a specific location in memory
constexpr unsigned write_str = 0xD; // Write a word(string) from a specific location in memory to the screen
constexpr unsigned load = 0x14; // Load a word from a specific location in memory to the accumulator
constexpr unsigned store = 0x15; // Store a word from the accumulator into a specific location in memory
constexpr unsigned add = 0x1E; /* Add a word from a specific location in memory to the word in the accumulator; store the
result in the accumulator */
constexpr unsigned subtract = 0x1F;
constexpr unsigned multiply = 0x20;
constexpr unsigned divide = 0x21;
constexpr unsigned modulo = 0x22;
constexpr unsigned branch = 0x28; // Branch to a specific location in the memory
constexpr unsigned branchneg = 0x29; // Branch if accumulator is negative
constexpr unsigned branchzero = 0x2A; // Branch if accumulator is zero
constexpr unsigned halt = 0x2B; // Halt the program when a task is completed
constexpr unsigned newline = 0x32; // Insert a new line
constexpr unsigned end = -0x1869F; // End the program execution
constexpr unsigned memory_size = 1000;
constexpr unsigned sml_debug = 0x2C; // SML debug
registers.h
int accumulator = 0;
unsigned instruction_counter = 0;
unsigned instruction_register = 0;
unsigned operation_code = 0;
unsigned operand = 0;
sml.h
#include "constants.h"
void memory_dump( int memory[memory_size], const unsigned &mem_size, const int &acc, const unsigned &ins_reg, \
const unsigned &ins_cnt, const unsigned &opr_code, const unsigned &opr );
void execute( int memory[memory_size], int &acc, unsigned &ins_reg, unsigned &ins_cnt, unsigned &opr_code, unsigned &opr ); // executes the statement in sequential manner
void evaluate( int memory[memory_size], int &acc, unsigned &ins_reg, unsigned &ins_cnt, unsigned &opr_code, unsigned &opr );
void display_welcome_message();
bool division_by_zero( int memory[ memory_size ], unsigned operand );
sml.cpp
#include <iostream>
#include <iomanip>
#include <string>
#include "sml.h"
int temp_cnt = 0; // holds instruction_counter when performing branch operation
std::string temp_str; // holds the string before it is written into the memory
bool debug = false;
void memory_dump( int memory[memory_size], const unsigned &mem_size, const int &acc, const unsigned &ins_reg, \
const unsigned &ins_cnt, const unsigned &opr_code, const unsigned &opr )
{
std::cout << "\nREGISTERS:\n";
std::cout << std::setw( 25 ) << std::left << std::setfill( ' ' ) << "accumulator" << std::showpos
<< std::setw( 5 ) << std::setfill( '0' ) << std::internal << acc << '\n';
std::cout << std::setw( 28 ) << std::left << std::setfill( ' ' )
<< "instruction counter" << std::noshowpos << std::setfill( '0' )
<< std::right << std::setw( 2 ) << ins_cnt << '\n';
std::cout << std::setw( 25 ) << std::left << std::setfill( ' ' )
<< "instruction register" << std::showpos << std::setw( 5 ) << std::setfill( '0' )
<< std::internal << ins_reg << '\n';
std::cout << std::setw( 28 ) << std::left << std::setfill( ' ' )
<< "operation code" << std::noshowpos << std::setfill( '0' )
<< std::right << std::setw( 2 ) << opr_code << '\n';
std::cout << std::setw( 28 ) << std::left << std::setfill( ' ' )
<< "operand" << std::noshowpos << std::setfill( '0' )
<< std::right << std::setw( 2 ) << opr << '\n';
std::cout << "\n\nMEMORY:\n";
std::cout << " ";
for( int i = 0; i != 10; ++i )
std::cout << std::setw( 6 ) << std::setfill( ' ') << std::right << i;
for( size_t i = 0; i != mem_size; ++i )
{
if( i % 10 == 0 )
std::cout << "\n" << std::setw( 3 ) << std::setfill( ' ' ) << i << " ";
std::cout << std::setw( 5 ) << std::setfill( '0' ) << std::showpos << std::internal << memory[ i ] << " ";
}
std::cout << std::endl;
}
void execute( int memory[memory_size], int &acc, unsigned &ins_reg, \
unsigned &ins_cnt, unsigned &opr_code, unsigned &opr )
{
int divisor;
while( memory[ ins_cnt ] != 0 )
{
ins_reg = memory[ ins_cnt++ ];
if( ins_reg < 1000 ) divisor = 0x10;
else if( ins_reg >= 1000 && ins_reg < 10000 ) divisor = 0x100;
else if( ins_reg >= 10000 && ins_reg < 100000 ) divisor = 0x1000;
opr_code = ins_reg / divisor;
opr = ins_reg % divisor ;
if( opr_code == halt )
break;
evaluate( memory, acc, ins_reg, ins_cnt, opr_code, opr );
if( debug )
memory_dump( memory, memory_size, acc, ins_reg, ins_cnt, \
opr_code, opr );
}
}
void evaluate( int memory[memory_size], int &acc, unsigned &ins_reg, \
unsigned &ins_cnt, unsigned &opr_code, unsigned &opr )
{
switch ( opr_code )
{
case read:
std::cin >> memory[ opr ];
break;
case read_str:
std::cin >> temp_str;
memory[ opr ] = temp_str.size();
for( int i = 1; i != temp_str.size() + 1; ++i )
memory[ opr + i ] = int( temp_str[ i - 1 ] );
break;
case write:
std::cout << memory[ opr ] << " ";
break;
case write_str:
for( int i = 0; i != memory[ opr ] + 1; ++i ) {
std::cout << char( memory[ opr + i ]);
}
break;
case load:
acc = memory[ opr ];
break;
case store:
memory[ opr ] = acc;
break;
case add:
acc += memory[ opr ];
break;
case subtract:
acc -= memory[ opr ];
break;
case multiply:
acc *= memory[ opr ];
break;
case divide:
if ( division_by_zero( memory, opr ) )
{
memory_dump( memory, memory_size, acc, ins_reg, ins_cnt, opr_code, opr );
exit( EXIT_FAILURE );
}
else
{
acc /= memory[ opr ];
break;
}
case modulo:
if( division_by_zero( memory, opr ) )
{
memory_dump( memory, memory_size, acc, ins_reg, ins_cnt, opr_code, opr );
exit( EXIT_FAILURE );
}
else
{
acc %= memory[ opr ];
break;
}
case branch:
temp_cnt = ins_cnt;
ins_cnt = opr;
execute( memory, acc, ins_reg, ins_cnt, opr_code, opr );
ins_cnt = temp_cnt;
break;
case branchneg:
if( acc < 0 )
{
temp_cnt = ins_cnt;
ins_cnt = opr;
execute( memory, acc, ins_reg, ins_cnt, opr_code, opr );
ins_cnt = temp_cnt;
}
break;
case branchzero:
if( acc == 0 )
{
temp_cnt = ins_cnt;
ins_cnt = opr;
execute( memory, acc, ins_reg, ins_cnt, opr_code, opr );
ins_cnt = temp_cnt;
}
break;
case newline:
std::cout << '\n' << std::flush;
break;
case sml_debug:
if ( opr == 1 ) debug = true;
else if ( opr == 0 ) debug = false;
else
{
std::cout << std::setw( 5 ) << std::setfill( ' ') << std::left << "***"
<< "Invalid debug mode"
<< std::setw( 5 ) << std::right << "***\n";
}
break;
default:
break;
}
}
void display_welcome_message () {
std::cout << "***" << " WELCOME TO SIMPLETRON! " << "***\n\n";
std::cout << std::setw( 5 ) << std::left << "***"
<< "Please enter your program one instruction"
<< std::setw( 5 ) << std::right << "***\n";
std::cout << std::setw( 5 ) << std::left << "***"
<< "(or data word) at a time. I will type the"
<< std::setw( 5 ) << std::right << "***\n";
std::cout << std::setw( 5 ) << std::left << "***"
<< "location number and a question mark (?)."
<< std::setw( 6 ) << std::right << "***\n";
std::cout << std::setw( 5 ) << std::left << "***"
<< "You then type the word for that location"
<< std::setw( 6 ) << std::right << "***\n";
std::cout << std::setw( 5 ) << std::left << "***"
<< "Type the sentinel -0x1869F to stop entering"
<< std::setw( 5 ) << std::right << "***\n";
std::cout << std::setw( 5 ) << std::left << "***"
<< "your program"
<< std::setw( 5 ) << std::right << "***";
std::cout << "\n\n" << std::flush;
}
bool division_by_zero( int memory[ memory_size ], unsigned operand )
{
if ( memory[ operand ] == 0 )
{
std::cout << std::setw( 5 ) << std::left << "***"
<< "Attempting division by zero"
<< std::setw( 5 ) << std::right << "***\n";
std::cout << std::setw( 5 ) << std::left << "***"
<< "Program terminated abnormally"
<< std::setw( 5 ) << std::right << "***\n";
std::cout << "\n";
return true;
}
return false;
}
main.cpp
#include <iostream>
#include <iomanip>
#include "registers.h"
#include "sml.h"
int main()
{
int memory[ memory_size ]{};
size_t memory_size = sizeof( memory )/ sizeof( memory[ 0 ] );
int temp;
display_welcome_message();
while( instruction_counter != memory_size )
{
std::cout << std::setw( 2 ) << std::setfill( '0' )
<< instruction_counter << " ? ";
std::cin >> std::hex >> temp;
if( temp == end ) {
break;
}
if( temp >= -0xB3E8 && temp < 0xB3E8 )
memory[ instruction_counter++ ] = temp;
else
continue;
}
instruction_counter = 0;
std::cout << std::setfill( ' ' );
std::cout << std::setw( 5 ) << std::left << "***"
<< "Program loaded into memory"
<< std::setw( 5 ) << std::right << "***\n";
std::cout << std::setw( 5 ) << std::left << "***"
<< "Program execution starts..."
<< std::setw( 5 ) << std::right << "***\n";
execute( memory, accumulator, instruction_register, instruction_counter, operation_code, operand );
std::cout << std::endl;
}
Réponses
Observations générales
Ce type particulier de problème est toujours intéressant à résoudre.
Depuis votre quatrième question, vous semblez éviter les cours. En C ++, les classes sont votre entrée dans la programmation orientée objet et les classes fournissent d'excellents outils. Comme @ G.Sliepen l'a déclaré dans sa critique, le simulateur serait bien meilleur s'il s'agissait d'une classe. Il n'y aurait aucun besoin de variables globales si le simulateur était implémenté en tant que classe. Les interfaces publiques pour execute(), evaluate()et memory_dump()seraient beaucoup plus simples puisque le tableau mémoire et les registres seraient des variables privées et il n'y aurait pas besoin de les passer dans la fonction.
Pour rendre le programme plus convivial, ajoutez un éditeur de ligne qui permet à l'utilisateur de modifier le programme du simulateur. De cette façon, le programme n'a pas besoin de quitter si le simulateur vide la mémoire. L'exécution du simulateur peut s'arrêter, l'utilisateur peut éditer la ligne puis redémarrer la simulation. Utilisez des exceptions plutôt que exit(EXIT_FAILURE);de ramener le programme à un état connu.
Vous voudrez peut-être consulter les réponses à cette question pour plus d'informations.
Évitez les variables globales
Actuellement, il y a au moins 8 variables globales dans le programme, dans registers.h:
int accumulator = 0;
unsigned instruction_counter = 0;
unsigned instruction_register = 0;
unsigned operation_code = 0;
unsigned operand = 0;
dans sml.cpp:
int temp_cnt = 0; // holds instruction_counter when performing branch operation
std::string temp_str; // holds the string before it is written into the memory
bool debug = false;
Il est très difficile de lire, d'écrire, de déboguer et de maintenir des programmes qui utilisent des variables globales. Les variables globales peuvent être modifiées par n'importe quelle fonction du programme et nécessitent donc que chaque fonction soit examinée avant d'apporter des modifications au code. En C et C ++, les variables globales ont un impact sur l'espace de noms et peuvent provoquer des erreurs de liaison si elles sont définies dans plusieurs fichiers. Les réponses à cette question stackoverflow fournissent une explication plus complète.
La plupart ou toutes ces variables globales pourraient être des variables privées si le simulateur était implémenté en tant que classe.
Les registres pourraient être implémentés sous la forme d'un tableau indexé par un enun.
typedef enum
{
ACCUMULATOR = 0,
INSTRUCTION_COUNTER = 1,
INSTRUCTION_REGISTER = 2,
OPERATION_CODE = 3,
OPERAND = 4,
REGISTER_COUNT = 5
} REGISTERS;
unsigned registers[static_cast<unsigned>(REGISTER_COUNT)];
registers[ACCUMULATOR] = 0;
Si le code de sml.cpp n'est pas converti en classe, il serait préférable de créer chacune de ces variables de staticsorte que leur portée ne soit que celle du fichier sml.cpp lui-même, pour le moment, elles pourraient être accessibles dans d'autres .cppfichiers comme main.cpp.
Les variables globales des registres doivent être déclarées dans sml.cppcar elles ne sont pas nécessaires à d'autres parties du programme telles que main.cpp.
Inclure les gardes
En C ++ ainsi que dans le langage de programmation C, le mécanisme d'importation de code #include FILEcopie en fait le code dans un fichier temporaire généré par le compilateur. Contrairement à certains autres langages modernes, C ++ (et C) inclura un fichier plusieurs fois. Pour éviter cela, les programmeurs utilisent des gardes d'inclusion qui peuvent avoir 2 formes:
la forme la plus portable consiste à intégrer le code dans une paire d'instructions pré-processeur
#ifndef SYMBOL
#define SYMBOL
// All other necessary code
#endif // SYMBOL
A popular form that is supported by most but not all C++ compilers is to put #pragma once at the top of the header file.
L'utilisation de l'une des 2 méthodes ci-dessus pour empêcher le contenu d'un fichier d'être inclus plusieurs fois est une bonne pratique pour la programmation C ++. Cela peut améliorer les temps de compilation si le fichier est inclus plusieurs fois, cela peut également empêcher les erreurs du compilateur et les erreurs de l'éditeur de liens.
Complexité
La fonction evaluate()est trop complexe (en fait trop) et les performances peuvent être améliorées. Si les valeurs d'opcode définies constants.hétaient dans l'ordre et commençant à zéro, un tableau de fonctions pourrait être utilisé pour implémenter chacun des opcodes. Ensuite, chaque opcode peut être évalué en simplifiant l'indexation dans ce tableau par opcode. Cela réduirait considérablement la quantité de code dans la fonction. Il fonctionnera plus rapidement car l'indexation dans un tableau est plus rapide que de passer par plusieurs instructions if dans le code d'assembly généré. Cela facilite également l'extension du jeu d'instructions.
Chiffres et tailles de mots
Je vois du code comme celui-ci dans votre code:
constexpr unsigned read = 0xA;
int accumulator = 0;
Cela signifie que vous associez la taille des mots à la taille d'un mot intsur la machine sur laquelle vous compilez votre code. Je créerais de nouveaux alias de type qui définissent explicitement la taille d'un mot signé et non signé dans votre machine simulée:
using sword = int32_t;
using uword = uint32_t;
Ensuite, il y a les chiffres. Vous dites que l'entrée de l'utilisateur est en hexadécimal, que les mots sont à 4 chiffres, mais d'après le code, il semble que ce soient des chiffres décimaux? Ce n'est pas très cohérent. La plupart des ordinateurs fonctionneraient avec des puissances de deux, ce qui rend également la simulation beaucoup plus rapide (les opérations de division et de module sont assez coûteuses).
Créer un classpour conserver l'état de la machine
Au lieu d'avoir beaucoup de fonctions hors classe auxquelles vous devez passer beaucoup de variables à chaque fois, il est plus logique de créer un classqui représente la machine simulée, et qui contient des variables membres pour les registres et la mémoire, comme alors:
class Machine {
int accumulator = 0;
unsigned instruction_counter = 0;
...
std::vector<int> memory(memory_size);
void memory_dump();
void evaluate();
public:
void load_program();
void execute();
};
Vous pouvez également déplacer toutes les constantes à l'intérieur class Machine, afin qu'elles ne polluent plus l'espace de noms global, en particulier lorsque vous avez des noms comme readet writequi occultent les fonctions POSIX.
Je déplacerais tout de sml.cppvers class Machine, sauf display_welcome_message(), ce qui devrait probablement être juste main.cppdedans, car cela ne concerne pas le fonctionnement de la machine.
Évitez les constantes magiques
Vous avez des noms propres pour toutes les constantes, sauf -0xB3E8et 0xB3E8. Qu'est-ce qui se passe avec ça? Donnez-leur également un nom.
Pensez à utiliser une bibliothèque de mise en forme
Créer une sortie joliment formatée à l'aide de iostreamfonctionnalités en C ++ est très ennuyeux. Cela nécessite beaucoup de code, des erreurs sont facilement commises et le code source semble terrible. Si vous pouvez déjà utiliser C ++ 20, je vous suggère fortement de commencer à utiliser std::format(), mais si vous ne pouvez pas, envisagez d'utiliser fmtlib , qui est la bibliothèque qui std::format()est basée sur et fonctionnera avec les versions antérieures de C ++. Cela signifie que vous pouvez réécrire votre code comme ceci:
std::cout << std::format("{:02} ? ", instruction_counter);
...
std::cout << std::format("{:*^40}\n", " Program loaded into memory ");
...
std::cout << std::format("{:*^40}\n", " Program execution starts... ");