Les objectifs de ce tutoriel sont les suivants :
- Découvrez comment l'ISA générée et les décodeurs binaires s'imbriquent.
- Écrivez le code C++ nécessaire pour créer un décodeur d'instructions complet pour RiscV RV32I qui combine les décodeurs ISA et binaires.
Comprendre le décodeur d'instructions
Le décodeur d'instructions est chargé, en fonction d'une adresse d'instruction, de lire le mot d'instruction à partir de la mémoire et de renvoyer une instance entièrement initialisée de l'Instruction
qui représente cette instruction.
Le décodeur de premier niveau implémente le generic::DecoderInterface
présenté ci-dessous :
// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
public:
// Return a decoded instruction for the given address. If there are errors
// in the instruciton decoding, the decoder should still produce an
// instruction that can be executed, but its semantic action function should
// set an error condition in the simulation when executed.
virtual Instruction *DecodeInstruction(uint64_t address) = 0;
virtual ~DecoderInterface() = default;
};
Comme vous pouvez le constater, il n'y a qu'une seule méthode à implémenter : cpp
virtual Instruction *DecodeInstruction(uint64_t address);
.
Voyons maintenant ce qui est fourni et ce qui est nécessaire au code généré.
Commencez par examiner la classe de premier niveau RiscV32IInstructionSet
dans le fichier riscv32i_decoder.h
, qui a été générée à la fin du tutoriel sur le décodeur ISA. Pour afficher à nouveau le contenu, accédez au répertoire de solution de ce tutoriel et reconstruisez-le.
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
Revenez maintenant au répertoire racine du dépôt, puis examinons les sources générées. Pour ce faire, modifiez le répertoire en bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(en supposant que vous vous trouvez sur un hôte x86. Pour les autres hôtes, k8-fastbuild sera une autre chaîne).
$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
Les quatre fichiers sources contenant le code C++ généré s'affichent :
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Ouvrez le premier fichier riscv32i_decoder.h
. Nous devons examiner trois classes :
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
Notez la dénomination des classes. Toutes les classes sont nommées en fonction de la version en casse Pascal du nom donné dans la déclaration "isa" de ce fichier :isa RiscV32I { ... }
Commençons par la classe RiscVIInstructionSet
. Elle est illustrée ci-dessous:
class RiscV32IInstructionSet {
public:
RiscV32IInstructionSet(ArchState *arch_state,
RiscV32IInstructionSetFactory *factory);
Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);
private:
std::unique_ptr<Riscv32Slot> riscv32_decoder_;
ArchState *arch_state_;
};
Cette classe ne contient aucune méthode virtuelle. Il s'agit donc d'une classe autonome, mais notez deux points. Tout d'abord, le constructeur accepte un pointeur vers une instance de la classe RiscV32IInstructionSetFactory
. Il s'agit d'une classe utilisée par le décodeur généré pour créer une instance de la classe RiscV32Slot
, qui permet de décoder toutes les instructions définies pour le slot RiscV32
, comme défini dans le fichier riscv32i.isa
. Deuxièmement, la méthode Decode
prend un paramètre supplémentaire de type pointeur vers RiscV32IEncodingBase
. Il s'agit d'une classe qui fournira l'interface entre le décodeur ISA généré dans le premier tutoriel et le décodeur binaire généré dans le deuxième atelier.
La classe RiscV32IInstructionSetFactory
est une classe abstraite à partir de laquelle nous devons dériver notre propre implémentation pour le décodeur complet. Dans la plupart des cas, cette classe est simple : il suffit de fournir une méthode pour appeler le constructeur de chaque classe de slot définie dans notre fichier .isa
. Dans notre cas, c'est très simple, car il n'existe qu'une seule classe de ce type : Riscv32Slot
(nom en casse Pascal riscv32
concaténé avec Slot
). La méthode n'est pas générée pour vous, car il existe certains cas d'utilisation avancés où il peut être utile de dériver une sous-classe du slot et d'appeler son constructeur à la place.
Nous verrons la classe finale RiscV32IEncodingBase
plus tard dans ce tutoriel, car elle fait l'objet d'un autre exercice.
Définir le décodeur d'instructions de premier niveau
Définir la classe de fabrique
Si vous avez reconstruit le projet pour le premier tutoriel, veillez à revenir au répertoire riscv_full_decoder
.
Ouvrez le fichier riscv32_decoder.h
. Tous les fichiers d'inclusion nécessaires ont déjà été ajoutés et les espaces de noms ont été configurés.
Après le commentaire marqué //Exercise 1 - step 1
, définissez la classe RiscV32IsaFactory
qui hérite de RiscV32IInstructionSetFactory
.
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
Ensuite, définissez le forçage pour CreateRiscv32Slot
. Étant donné que nous n'utilisons aucune classe dérivée de Riscv32Slot
, nous allouons simplement une nouvelle instance à l'aide de std::make_unique
.
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est disponible sur cette page.
Définir la classe de décodeur
Déclarations de constructeurs, de destructeurs et de méthodes
Il est maintenant temps de définir la classe de décodeur. Dans le même fichier que ci-dessus, accédez à la déclaration de RiscV32Decoder
. Développez la déclaration en une définition de classe où RiscV32Decoder
hérite de generic::DecoderInterface
.
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
Ensuite, avant d'écrire le constructeur, examinons rapidement le code généré dans notre deuxième tutoriel sur le décodeur binaire. En plus de toutes les fonctions Extract
, il existe la fonction DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Cette fonction prend le mot d'instruction à décoder et renvoie l'opcode correspondant à cette instruction. En revanche, la classe DecodeInterface
implémentée par RiscV32Decoder
ne transmet qu'une adresse. Par conséquent, la classe RiscV32Decoder
doit pouvoir accéder à la mémoire pour lire le mot d'instruction à transmettre à DecodeRiscVInst32()
. Dans ce projet, l'accès à la mémoire passe par une interface de mémoire simple définie dans .../mpact/sim/util/memory
, bien nommée util::MemoryInterface
, comme illustré ci-dessous:
// Load data from address into the DataBuffer, then schedule the Instruction
// inst (if not nullptr) to be executed (using the function delay line) with
// context. The size of the data access is based on size of the data buffer.
virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
ReferenceCount *context) = 0;
De plus, nous devons pouvoir transmettre une instance de classe state
aux constructeurs des autres classes de décodeur. La classe d'état appropriée est la classe riscv::RiscVState
, qui dérive de generic::ArchState
, avec des fonctionnalités supplémentaires pour RiscV. Cela signifie que nous devons déclarer le constructeur afin qu'il puisse accepter un pointeur vers state
et memory
:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
Supprimez le constructeur par défaut et remplacez le destructeur :
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
Déclarez ensuite la méthode DecodeInstruction
que nous devons remplacer à partir de generic::DecoderInterface
.
generic::Instruction *DecodeInstruction(uint64_t address) override;
Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est disponible sur cette page.
Définitions des membres de données
La classe RiscV32Decoder
aura besoin de membres de données privées pour stocker les paramètres du constructeur et un pointeur vers la classe de fabrique.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
Il a également besoin d'un pointeur vers la classe d'encodage dérivée de RiscV32IEncodingBase
, que nous appellerons RiscV32IEncoding
(nous l'implémenterons dans l'exercice 2). De plus, il a besoin d'un pointeur vers une instance de RiscV32IInstructionSet
. Ajoutez donc :
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
Enfin, nous devons définir un membre de données à utiliser avec notre interface mémoire:
generic::DataBuffer *inst_db_;
Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est disponible sur cette page.
Définir les méthodes de la classe Decoder
Il est ensuite temps d'implémenter le constructeur, le destructeur et la méthode DecodeInstruction
. Ouvrez le fichier riscv32_decoder.cc
. Le fichier contient déjà des méthodes vides, ainsi que des déclarations d'espace de noms et quelques déclarations using
.
Définition du constructeur
Le constructeur n'a besoin que d'initialiser les membres des données. Commencez par initialiser state_
et memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
Allouez ensuite des instances de chacune des classes associées au décodeur, en transmettant les paramètres appropriés.
// Allocate the isa factory class, the top level isa decoder instance, and
// the encoding parser.
riscv_isa_factory_ = new RiscV32IsaFactory();
riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
riscv_encoding_ = new RiscV32IEncoding(state);
Enfin, allouez l'instance DataBuffer
. Il est alloué à l'aide d'une usine accessible via le membre state_
. Nous allouons une mémoire tampon de données de taille suffisante pour stocker un seul uint32_t
, car c'est la taille du mot d'instruction.
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
Définition du destructeur
Le destructeur est simple. Il suffit de libérer les objets que nous avons alloués dans le constructeur, mais avec une petite particularité. L'instance de tampon de données est comptabilisée par référence. Par conséquent, au lieu d'appeler delete
sur ce pointeur, nous DecRef()
l'objet :
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
Définition de la méthode
Dans notre cas, l'implémentation de cette méthode est assez simple. Nous supposons que l'adresse est correctement alignée et qu'aucune vérification d'erreur supplémentaire n'est requise.
Tout d'abord, l'instruction doit être extraite de la mémoire à l'aide de l'interface mémoire et de l'instance DataBuffer
.
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
Ensuite, nous appelons l'instance RiscVIEncoding
pour analyser le mot d'instruction, ce qui doit être fait avant d'appeler le décodeur ISA lui-même. Rappelez-vous que le décodeur ISA appelle directement l'instance RiscVIEncoding
pour obtenir l'opcode et les opérandes spécifiés par le mot d'instruction. Nous n'avons pas encore implémenté cette classe, mais utilisons void ParseInstruction(uint32_t)
comme méthode.
riscv_encoding_->ParseInstruction(iword);
Enfin, nous appelons le décodeur ISA en transmettant l'adresse et la classe d'encodage.
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est disponible ici.
Classe d'encodage
La classe d'encodage implémente une interface utilisée par la classe de décodeur pour obtenir l'opcode d'instruction, ses opérandes source et de destination, ainsi que les opérandes de ressources. Ces objets dépendent tous des informations du décodeur de format binaire, telles que le code d'opération, les valeurs de champs spécifiques du mot d'instruction, etc. Ces objets sont séparés de la classe du décodeur pour assurer leur agnostique et permettre la prise en charge de différents schémas d'encodage à l'avenir.
RiscV32IEncodingBase
est une classe abstraite. L'ensemble des méthodes que nous devons implémenter dans notre classe dérivée est présenté ci-dessous.
class RiscV32IEncodingBase {
public:
virtual ~RiscV32IEncodingBase() = default;
virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;
virtual ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end) = 0;
virtual ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end) = 0;
virtual PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op) = 0;
virtual SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no) = 0;
virtual DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency) = 0;
virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no) = 0;
};
À première vue, cela semble un peu compliqué, en particulier avec le nombre de paramètres, mais pour une architecture simple comme RiscV, nous ignorons en fait la plupart des paramètres, car leurs valeurs seront implicites.
Passons en revue chacune des méthodes.
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
La méthode GetOpcode
renvoie le membre OpcodeEnum
pour l'instruction en cours, identifiant le code d'opération de l'instruction. La classe OpcodeEnum
est définie dans le fichier de décodeur ISA riscv32i_enums.h
généré. La méthode utilise deux paramètres, qui peuvent tous deux être ignorés dans le cadre de notre travail. La première est le type d'emplacement (une classe d'énumération également définie dans riscv32i_enums.h
), qui, comme RiscV ne comporte qu'un seul emplacement, n'a qu'une seule valeur possible : SlotEnum::kRiscv32
. Le second est le numéro d'instance de l'emplacement (au cas où il existe plusieurs instances de l'emplacement, ce qui peut se produire dans certaines architectures VLIW).
ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end)
ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end);
Les deux méthodes suivantes sont utilisées pour modéliser les ressources matérielles du processeur afin d'améliorer la précision des cycles. Pour nos exercices de tutoriel, nous ne les utiliserons pas. Par conséquent, lors de l'implémentation, ils seront remplacés par des bouchons, qui renverront nullptr
.
PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op);
SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no);
DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency);
Ces trois méthodes renvoient des pointeurs vers des objets d'opérandes qui sont utilisés dans les fonctions sémantiques d'instruction pour accéder à la valeur de n'importe quel opérande de prédicat d'instruction, à chacun des opérandes sources d'instruction et à écrire de nouvelles valeurs dans les opérandes de destination d'instruction. Étant donné que RiscV n'utilise pas de prédicats d'instruction, cette méthode ne doit renvoyer que nullptr
.
Le modèle des paramètres est similaire pour toutes ces fonctions. Tout d'abord, comme pour GetOpcode
, l'emplacement et l'entrée sont transmis. Ensuite, l'opcode de l'instruction pour laquelle l'opérande doit être créé. Cette méthode n'est utilisée que si les différents opérandes doivent renvoyer différents objets d'opérande pour les mêmes types d'opérandes, ce qui n'est pas le cas de ce simulateur RiscV.
Vient ensuite l'entrée d'énumération des opérandes "Prédicat", "Source" et "Destination", qui identifie l'opérande à créer. Ils proviennent des trois OpEnums dans riscv32i_enums.h
, comme indiqué ci-dessous :
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Si vous revenez au fichier riscv32.isa
, vous remarquerez qu'il correspond aux ensembles de noms d'opérandes source et de destination utilisés dans la déclaration de chaque instruction. En utilisant des noms d'opérandes différents pour les opérandes qui représentent différents champs d'octets et types d'opérandes, l'écriture de la classe d'encodage est simplifiée, car le membre de l'énumération détermine de manière unique le type d'opérande exact à renvoyer, et il n'est pas nécessaire de prendre en compte les valeurs des paramètres d'emplacement, d'entrée ou d'opcode.
Enfin, pour les opérandes source et de destination, la position ordinale de l'opérande est transmise (là encore, nous pouvons l'ignorer), et pour l'opérande de destination, la latence (en cycles) qui s'écoule entre le moment où l'instruction est émise et le moment où le résultat de destination est disponible pour les instructions suivantes. Dans notre simulateur, cette latence sera de 0, ce qui signifie que l'instruction écrit immédiatement le résultat dans le registre.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
La fonction finale permet d'obtenir la latence d'un opérande de destination particulier s'il a été spécifié comme *
dans le fichier .isa
. Cela est inhabituel et n'est pas utilisé pour ce simulateur RiscV. Par conséquent, notre implémentation de cette fonction ne renverra que 0.
Définir la classe d'encodage
Fichier d'en-tête (.h)
Méthodes
Ouvrez le fichier riscv32i_encoding.h
. Tous les fichiers d'inclusion nécessaires ont déjà été ajoutés et les espaces de noms ont été configurés. Tout ajout de code est effectué après le commentaire // Exercise 2.
.
Commençons par définir une classe RiscV32IEncoding
qui hérite de l'interface générée.
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
Ensuite, le constructeur doit utiliser un pointeur vers l'instance d'état, dans ce cas un pointeur vers riscv::RiscVState
. Le destructeur par défaut doit être utilisé.
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
Avant d'ajouter toutes les méthodes d'interface, ajoutons la méthode appelée par RiscV32Decoder
pour analyser l'instruction:
void ParseInstruction(uint32_t inst_word);
Ajoutons ensuite les méthodes qui présentent des remplacements simples, tout en supprimant les noms des paramètres qui ne sont pas utilisés:
// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
SimpleResourceVector &,
int) override {
return nullptr;
}
ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
ComplexResourceEnum ,
int, int) override {
return nullptr;
}
PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
PredOpEnum) override {
return nullptr;
}
int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }
Enfin, ajoutez les autres forçages de méthode de l'interface publique, mais avec les implémentations différées vers le fichier .cc.
OpcodeEnum GetOpcode(SlotEnum, int) override;
SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
SourceOpEnum source_op, int) override;
DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
DestOpEnum dest_op, int,
int latency) override;
Afin de simplifier l'implémentation de chacune des méthodes getter d'opérande, nous allons créer deux tableaux de appelables (objets de fonction) indexés respectivement par la valeur numérique des membres SourceOpEnum
et DestOpEnum
.
De cette façon, le corps de ces deux méthodes se réduit à appeler l'objet de fonction pour la valeur d'énumération transmise et à renvoyer sa valeur de retour.
Pour organiser l'initialisation de ces deux tableaux, nous définissons deux méthodes privées qui seront appelées à partir du constructeur comme suit:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
Membres des données
Les membres de données requis sont les suivants :
state_
pour contenir la valeurriscv::RiscVState *
.inst_word_
de typeuint32_t
qui contient la valeur du mot d'instruction actuel.opcode_
pour contenir le code d'opération de l'instruction actuelle qui est mis à jour par la méthodeParseInstruction
. Il est de typeOpcodeEnum
.source_op_getters_
Tableau permettant de stocker les appelables utilisés pour obtenir des objets d'opérande source. Le type des éléments du tableau estabsl::AnyInvocable<SourceOperandInterface *>()>
.dest_op_getters_
: un tableau pour stocker les éléments appelables utilisés pour obtenir des objets d'opérande de destination. Le type des éléments du tableau estabsl::AnyInvocable<DestinationOperandInterface *>()>
.xreg_alias
un tableau de noms ABI de registres entiers RiscV, par exemple : "zero" et "ra" au lieu de "x0" et "x1".
riscv::RiscVState *state_;
uint32_t inst_word_;
OpcodeEnum opcode_;
absl::AnyInvocable<SourceOperandInterface *()>
source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
absl::AnyInvocable<DestinationOperandInterface *(int)>
dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];
const std::string xreg_alias_[32] = {
"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0",
"a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5",
"s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};
Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est disponible ici.
Fichier source (.cc).
Ouvrez le fichier riscv32i_encoding.cc
. Tous les fichiers d'inclusion nécessaires ont déjà été ajoutés et les espaces de noms ont été configurés. Tout ajout de code est effectué après le commentaire // Exercise 2.
.
Fonctions de l'outil d'aide
Nous allons commencer par écrire quelques fonctions d'assistance que nous utilisons pour créer des opérandes de registre source et de destination. Ils seront modélisés sur le type de registre et appelleront l'objet RiscVState
pour obtenir un gestionnaire de l'objet de registre, puis appelleront une méthode de fabrique d'opérandes dans l'objet de registre.
Commençons par les assistants d'opérande de destination:
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency);
}
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency, op_name);
}
Comme vous pouvez le constater, il existe deux fonctions d'assistance. Le second utilise un paramètre op_name
supplémentaire qui permet à l'opérande d'avoir un nom ou une représentation de chaîne différent de celui du registre sous-jacent.
De même pour les assistants d'opérande source :
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand();
return op;
}
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand(op_name);
return op;
}
Fonctions de constructeur et d'interface
Le constructeur et les fonctions d'interface sont très simples. Le constructeur appelle simplement les deux méthodes d'initialisation pour initialiser les tableaux callables des getters d'opérandes.
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
stocke le mot d'instruction, puis l'opcode qu'il obtient en appelant le code généré par le décodeur binaire.
// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
inst_word_ = inst_word;
opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}
Enfin, les getters d'opérande renvoient la valeur de la fonction getter qu'il appelle en fonction de la recherche dans le tableau à l'aide de la valeur d'énumération de l'opérande source/destination.
DestinationOperandInterface *RiscV32IEncoding::GetDestination(
SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
return dest_op_getters_[static_cast<int>(dest_op)](latency);
}
SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
SourceOpEnum source_op, int) {
return source_op_getters_[static_cast<int>(source_op)]();
}
Méthodes d'initialisation de tableaux
Comme vous l'avez peut-être deviné, la majeure partie du travail consiste à initialiser les tableaux de getters, mais ne vous inquiétez pas, cela se fait à l'aide d'un modèle simple et répétitif. Commençons par InitializeDestinationOpGetters()
, car il n'y a que quelques opérateurs de destination.
Rappelez la classe DestOpEnum
générée à partir de riscv32i_enums.h
:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Pour dest_op_getters_
, nous devons initialiser quatre entrées, une pour kNone
, kCsr
, kNextPc
et kRd
. Pour plus de commodité, chaque entrée est initialisée avec un lambda, mais vous pouvez également utiliser n'importe quelle autre forme de fonction appelable. La signature du lambda est void(int latency)
.
Jusqu'à présent, nous n'avons pas beaucoup parlé des différents types d'opérandes de destination définis dans MPACT-Sim. Pour cet exercice, nous n'utiliserons que deux types : generic::RegisterDestinationOperand
défini dans register.h
et generic::DevNullOperand
défini dans devnull_operand.h
.
Les détails de ces opérandes ne sont pas vraiment importants pour le moment, sauf que le premier est utilisé pour écrire dans les registres, et que le second ignore toutes les écritures.
La première entrée pour kNone
est simple : il suffit de renvoyer une valeur nullptr et éventuellement de consigner une erreur.
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
Ensuite, voici kCsr
. Ici, nous allons tricher un peu. Le programme "Hello World" ne repose sur aucune mise à jour CSR réelle, mais il existe un code standard qui exécute des instructions CSR. La solution consiste à le simuler en utilisant un registre standard nommé "CSR" et à y transmettre toutes les écritures de ce type.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
kNextPc
vient ensuite, et fait référence au registre "pc". Il est utilisé comme cible pour toutes les instructions de branchement et de saut. Le nom est défini dans RiscVState
comme kPcName
.
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
Enfin, il y a l'opérande de destination kRd
. Dans riscv32i.isa
, l'opérande rd
n'est utilisée que pour faire référence au registre d'entier encodé dans le champ "rd" du mot d'instruction. Il n'y a donc aucune ambiguïté quant à ce à quoi il fait référence. Il n'y a qu'une seule complication. Le registre x0
(nom ABI zero
) est câblé sur 0. Nous utilisons donc DevNullOperand
pour ce registre.
Dans ce getter, nous extrayons d'abord la valeur du champ rd
à l'aide de la méthode Extract
générée à partir du fichier .bin_fmt. Si la valeur est 0, nous renvoyons un opérande "DevNull". Sinon, nous renvoyons l'opérande de registre correct, en veillant à utiliser l'alias de registre approprié comme nom d'opérande.
dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
// First extract register number from rd field.
int num = inst32_format::ExtractRd(inst_word_);
// For register x0, return the DevNull operand.
if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
// Return the proper register operand.
return GetRegisterDestinationOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
xreg_alias_[num]);
)
}
}
Passons maintenant à la méthode InitializeSourceOperandGetters()
, où le schéma est à peu près le même, mais les détails diffèrent légèrement.
Examinons d'abord l'élément SourceOpEnum
généré à partir de riscv32i.isa
dans le premier tutoriel:
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
En examinant les membres, en plus de kNone
, ils se répartissent en deux groupes. L'un est les opérandes immédiats: kBimm12
, kImm12
, kJimm20
, kSimm12
, kUimm20
et kUimm5
. Les autres sont des opérandes de registre : kCsr
, kRs1
et kRs2
.
L'opérande kNone
est traitée comme les opérandes de destination : renvoyez un nullptr.
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
Passons maintenant aux opérandes de registre. Nous allons gérer kCsr
de la même manière que nous avons géré les opérateurs de destination correspondants. Il vous suffit d'appeler la fonction d'assistance en utilisant "CSR" comme nom de registre.
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
Les opérandes kRs1
et kRs2
sont gérés de manière équivalente à kRd
, sauf que, même si nous ne souhaitions pas mettre à jour x0
(ou zero
), nous voulons nous assurer que nous lisons toujours 0 à partir de cet opérande. Pour ce faire, nous allons utiliser la classe generic::IntLiteralOperand<>
définie dans literal_operand.h
.
Cette opérande permet de stocker une valeur littérale (par opposition à une valeur immédiate simulée). Sinon, le modèle est le même: commencez par extraire la valeur rs1/rs2 du mot d'instruction. Si elle est égale à zéro, renvoyez l'opérande littéral avec un paramètre de modèle 0. Sinon, renvoyez un opérande source de registre standard à l'aide de la fonction d'assistance, en utilisant l'alias "abi" comme nom d'opérande.
source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs1(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs2(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
Enfin, nous gérons les différents opérandes immédiats. Les valeurs immédiates sont stockées dans des instances de la classe generic::ImmediateOperand<>
définie dans immediate_operand.h
.
La seule différence entre les différents getters pour les opérandes immédiats est la fonction d'extraction utilisée et si le type de stockage est signé ou non, en fonction du champ de bits.
// Immediates.
source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractBImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractImm12(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm5(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractJImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractSImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm32(inst_word_));
};
}
Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est disponible sur cette page.
Ce tutoriel est maintenant terminé. Nous espérons qu'elle vous a été utile.