Les objectifs de ce tutoriel sont les suivants:
- Découvrez comment l'ISA générée et les décodeurs binaires s'assemblent.
- Écrire 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 de la mémoire et renvoyer une instance entièrement initialisée du
Instruction
qui représente cette instruction.
Le décodeur de niveau supérieur implémente l'élément 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 voir, il n'y a qu'une seule méthode à implémenter: cpp
virtual Instruction *DecodeInstruction(uint64_t address);
Voyons maintenant les éléments fournis et les éléments nécessaires au code généré.
Tout d'abord, considérons la classe de premier niveau RiscV32IInstructionSet
dans le fichier.
riscv32i_decoder.h
, généré à la fin du tutoriel sur la
décodeur ISA. Pour revoir le contenu, accédez au répertoire des solutions
ce tutoriel et tout recompiler.
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
Remettez votre répertoire à la racine du dépôt, puis voyons
aux sources générées. Pour cela, remplacez
le répertoire par
bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(en supposant que vous utilisez un ordinateur x86
host - 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
. Il y a trois classes que nous
vous devez examiner:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
Notez la dénomination des classes. Toutes les classes sont nommées d'après le nom
Version du nom indiqué dans le texte "isa" (pascal, en majuscules et minuscules) dans 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_;
};
Il n'y a pas de méthodes virtuelles dans cette classe. Il s'agit donc d'une classe autonome, mais
vous remarquerez deux choses. Tout d'abord, le constructeur utilise un pointeur vers une instance de
RiscV32IInstructionSetFactory
. Il s'agit d'une classe que le
decoder utilise pour créer une instance de la classe RiscV32Slot
, qui permet de
décoder toutes les instructions définies pour slot RiscV32
, telles que définies dans
riscv32i.isa
. Deuxièmement, la méthode Decode
utilise un paramètre supplémentaire
de type pointeur vers RiscV32IEncodingBase
, il s'agit d'une classe qui fournira
entre le décodeur ISA généré dans le premier tutoriel et le fichier binaire
généré dans le deuxième atelier.
La classe RiscV32IInstructionSetFactory
est une classe abstraite à partir de laquelle nous
nous devons obtenir notre propre implémentation
pour le décodeur complet. Dans la plupart des cas,
est simple: il vous suffit de fournir une méthode permettant d'appeler le constructeur pour chaque
d'emplacement définie dans le fichier .isa
. Dans notre cas, c'est très simple, car
n'est qu'une seule de ce type de classe: Riscv32Slot
(pascal en casse du nom riscv32
concaténé avec Slot
). La méthode n'est pas générée automatiquement
certains cas d'utilisation avancés pour lesquels il peut être utile de dériver une sous-classe
à partir de l'emplacement et appeler son constructeur à la place.
Nous aborderons la dernière classe RiscV32IEncodingBase
plus tard dans ce
car c'est le sujet d'un autre exercice.
Définir un décodeur d'instructions de premier niveau
Définir la classe de fabrique
Si vous avez recréé le projet pour le premier tutoriel, veillez à revenir à
le 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 que les espaces de noms ont été configurés.
Après le commentaire marqué //Exercise 1 - step 1
, définissez la classe
RiscV32IsaFactory
héritant de RiscV32IInstructionSetFactory
.
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
Ensuite, définissez le forçage pour CreateRiscv32Slot
. Puisque nous n'utilisons aucune
dérivées de Riscv32Slot
, il nous suffit d'allouer 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 cliquez ici.
Définir la classe du décodeur
Constructeurs, destructeurs et déclarations de méthode
Vous devez maintenant définir la classe du décodeur. Dans le même fichier que celui indiqué ci-dessus, accédez à
déclaration de RiscV32Decoder
. Développer la déclaration en une définition de classe
où RiscV32Decoder
hérite de generic::DecoderInterface
.
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
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
Extract
, il y a la fonction DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Cette fonction prend l'instruction à décoder et renvoie
le code d’opération qui
correspond à cette instruction. D'un autre côté,
La classe DecodeInterface
qui RiscV32Decoder
implémente uniquement les cartes dans une
adresse e-mail. Ainsi, la classe RiscV32Decoder
doit pouvoir accéder à la mémoire
Lisez le mot d'instruction pour le 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
porte bien le nom util::MemoryInterface
, vu 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 être en mesure de transmettre une instance de classe state
à la
les constructeurs des autres classes de décodeur. La classe d'état appropriée est
riscv::RiscVState
, qui dérive de generic::ArchState
, avec en plus
pour RiscV. Cela signifie que nous devons déclarer le constructeur
peut utiliser 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
à remplacer
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 cliquez ici.
Définitions des membres des données
La classe RiscV32Decoder
a besoin de membres de données privées pour stocker la
les paramètres du constructeur et un pointeur vers la classe Factory.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
Il a également besoin d'un pointeur vers la classe d'encodage dérivée de
RiscV32IEncodingBase
, appelons cette méthode RiscV32IEncoding
(nous implémenterons
dans l'exercice 2). De plus, il a besoin d'un pointeur vers une instance de
RiscV32IInstructionSet
, ajoutez donc les éléments suivants:
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 cliquez ici.
Définir les méthodes de la classe Decoder
Ensuite, il est temps d'implémenter le constructeur, le destructeur et le
DecodeInstruction
. Ouvrez le fichier riscv32_decoder.cc
. Le vide
déjà présentes dans le fichier, ainsi que les déclarations d'espace de noms et quelques
sur using
déclarations.
Constructeur – Définition
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 lié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 fabrique
accessible via le membre state_
. Nous allouons un tampon de données de taille
un seul uint32_t
, qui correspond à 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 alloués dans le constructeur.
mais avec une seule touche. L'instance de tampon de données est comptée comme référence. Au lieu de cela,
en appelant 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 partirons du principe que l'adresse est correctement alignée et qu'aucune vérification d'erreur supplémentaire n'est effectuée obligatoire.
Tout d'abord, l'instruction doit être extraite de la mémoire
interface et 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 l'instruction,
ce qui doit être fait avant d'appeler
le décodeur ISA lui-même. Pour rappel, l'ISA
le décodeur appelle directement l'instance RiscVIEncoding
pour obtenir l'opération.
et les opérandes spécifiés
par le mot d'instruction. Nous n'avons pas encore implémenté cette fonctionnalité.
, 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 Encoding.
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 cliquez ici.
La classe d'encodage
La classe d'encodage implémente une interface utilisée par la classe du décodeur. pour obtenir le code d'opération d'instruction, ses opérandes source et de destination, et les opérandes de ressource. Ces objets dépendent tous des informations du binaire le décodeur, comme le code d'opération, les valeurs de champs spécifiques dans d'instruction, etc. Elle est séparée de la classe du décodeur sont compatibles avec différents schémas d'encodage. à l'avenir.
RiscV32IEncodingBase
est une classe abstraite. L'ensemble des méthodes
que nous devons
implémentée dans notre classe dérivée est présentée 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é, surtout avec le nombre de mais pour une architecture simple comme RiscV, nous ignorons la plupart les paramètres, car leurs valeurs seront implicites.
Examinons chacune de ces méthodes l'une après l'autre.
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
La méthode GetOpcode
renvoie le membre OpcodeEnum
pour le membre actuel
l'instruction, identifiant son code d'opération. La classe OpcodeEnum
est
défini dans le fichier riscv32i_enums.h
du décodeur généré. La méthode prend
deux paramètres, qui peuvent tous deux être ignorés pour nos besoins. Le premier de
correspond au type d'emplacement (une classe d'énumération également définie dans riscv32i_enums.h
),
qui, puisque 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
il y a plusieurs instances de l'emplacement, ce qui peut se produire dans certains VLIW
d'architectures).
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 dans le processeur
afin d’améliorer la précision du cycle. Pour les exercices de ce tutoriel, nous n'utiliserons pas
Dans l'implémentation, ils seront donc bouchonnés, renvoyant ainsi 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 opérandes utilisés dans
les fonctions sémantiques d'instructions pour accéder à la valeur d'une instruction
l'opérande de prédicat, chacun des opérandes sources d'instructions, et écrivent de nouveaux
aux opérandes de destination de l'instruction. Puisque RiscV n'utilise pas
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
GetOpcode
, l'emplacement et l'entrée sont transmis. Ensuite, le code d’opération du
pour laquelle l'opérande doit être créé. Il n'est utilisé que si le
différents opérandes doivent renvoyer différents objets d'opérande pour le même opérande.
ce qui n'est pas le cas pour ce simulateur RiscV.
Vient ensuite l'entrée d'énumération d'opérande "Prédicat, Source" et "Destination"
identifie l'opérande à créer. Ceux-ci proviennent des trois
OpEnums dans riscv32i_enums.h
, comme illustré 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 regardez à nouveau
riscv32.isa
, vous remarquerez qu'ils correspondent aux ensembles de données source et de destination
noms d'opérandes utilisés dans la déclaration de chaque instruction. En utilisant différentes
noms d'opérandes représentant différents champs de bits et opérandes
il facilite l'écriture de la classe d'encodage, car le membre enum est
détermine le type exact d'opérande à renvoyer, et il n'est pas nécessaire de
prendre en compte les valeurs des paramètres
d'emplacement, d'entrée ou de code d'opération.
Enfin, pour les opérandes source et de destination, la position ordinale du l'opérande est transmis (là encore, nous pouvons l'ignorer), et pour la destination, opérande, la latence (en cycles) qui s'écoule entre le moment où l'instruction est émis, et le résultat de la destination est disponible pour les instructions suivantes. Dans notre simulateur, cette latence est égale à 0, ce qui signifie que l'instruction écrit le résultat immédiatement dans le registre.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
La fonction finale est utilisée pour obtenir la latence d'une destination particulière
opérande s'il a été spécifié comme *
dans le fichier .isa
. Ce n'est pas courant,
et n'est pas utilisé pour ce simulateur RiscV. Notre implémentation de cette fonction
renvoie simplement 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 que les espaces de noms ont été configurés. Tout ajout de code est
a suivi le commentaire // Exercise 2.
Commençons par définir une classe RiscV32IEncoding
qui hérite de la classe
via l'interface générée par Google.
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 en ignorant 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 sont 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 callables (objets fonction) indexés par
valeur numérique des membres SourceOpEnum
et DestOpEnum
respectivement.
De cette façon, les corps de ces objets
aux méthodes sont réduits à appeler
objet fonction de la valeur d'énumération transmise et renvoyant son résultat
.
Pour organiser l'initialisation de ces deux tableaux, nous définissons deux tableaux privés 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 de la de l'instruction.opcode_
pour contenir le code d'opération de l'instruction actuelle qui est mise à jour par la méthodeParseInstruction
. Elle est de typeOpcodeEnum
.source_op_getters_
: un tableau pour stocker les appelables utilisés pour obtenir la source opérandes. Le type des éléments du tableau estabsl::AnyInvocable<SourceOperandInterface *>()>
dest_op_getters_
: un tableau pour stocker les appelables utilisés pour obtenir opérandes de destination. Le type des éléments du tableau estabsl::AnyInvocable<DestinationOperandInterface *>()>
xreg_alias
: tableau de noms d'ABI de registre d'entiers RiscV, par exemple "zéro" 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 cliquez ici.
Fichier source (.cc).
Ouvrez le fichier riscv32i_encoding.cc
. Tous les fichiers d'inclusion nécessaires
ont déjà été ajoutés et que les espaces de noms ont été configurés. Tout ajout de code est
a suivi le commentaire // Exercise 2.
Fonctions de l'outil d'aide
Nous commencerons par écrire quelques fonctions d'assistance que nous utiliserons pour créer
les opérandes de registre
source et de destination. Celles-ci seront modélisées
un type d'enregistrement et appelle l'objet RiscVState
pour obtenir un handle vers
enregistrer un objet, puis appeler 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 voir, il existe deux fonctions d'assistance. La seconde prend une couche supplémentaire
Paramètre op_name
qui permet à l'opérande d'avoir un nom ou une chaîne différents
que le registre sous-jacent.
De même pour les assistants d'opérande sources:
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. Constructeur appelle simplement les deux méthodes d'initialisation pour initialiser les tableaux appelables pour les getters d'opérande.
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 tableau
Comme vous l'avez peut-être deviné, la majeure partie du travail consiste à initialiser le getter
des tableaux, mais ne vous inquiétez pas, vous utilisez
un modèle facile à répéter.
commencer par InitializeDestinationOpGetters()
, car il n'existe
deux opérandes 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 une
lambda, mais vous pouvez également utiliser n'importe quelle autre forme d'appelable. La signature
du lambda est void(int latency)
.
Jusqu'à présent, nous n'avons pas beaucoup parlé des différents types de destinations
d'opérandes définis dans MPACT-Sim. Pour cet exercice, nous n'utiliserons
types: generic::RegisterDestinationOperand
défini dans
register.h
,
et generic::DevNullOperand
définis dans
devnull_operand.h
.
Les détails de ces opérandes ne sont pas vraiment importants pour le moment, sauf que les
Le premier permet d'écrire dans les registres, tandis 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,
consignez une erreur.
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
Vient ensuite kCsr
. Ici, nous allons dupliquer un peu. Hello World programme
ne repose sur aucune mise à jour de requête de signature de certificat réelle, mais il existe du code récurrent
exécuter les instructions CSR. La solution consiste à le simuler
registre standard nommé "CSR" et canaliser toutes ces écritures.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
Vient ensuite kNextPc
, qui fait référence au "PC" registre. Il est utilisé comme cible
pour toutes les instructions relatives aux branches et aux sauts. Dans RiscVState
, le nom est défini comme suit :
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é que pour faire référence au registre d'entiers encodés dans "rd" champ
du mot d'instruction, il n'y a donc pas d'ambiguïté à laquelle il se réfère. Il y
n'est qu'une des complications. L'enregistrement de x0
(nom Abi zero
) est câblé sur 0.
Nous utilisons donc DevNullOperand
pour ce registre.
Dans cet getter, nous allons d'abord extraire 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
renvoie une valeur « DevNull » opérande, sinon nous renvoyons
l'opérande de registre correct,
veillez à 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 modèle est
à peu près les mêmes, mais
les détails diffèrent légèrement.
Commençons par examiner le 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,
};
À l'examen, les membres se répartissent en deux groupes en plus de kNone
. Un
correspond aux 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 géré comme pour les opérandes de destination : renvoie une
"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 traiterons les kCsr
similaires
à la façon dont nous avons géré les opérandes de destination correspondants. Il suffit d'appeler la méthode
la fonction d'assistance "CSR" comme nom d'enregistrement.
// 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 la même manière que kRd
, sauf que
alors que nous ne voulions pas mettre à jour x0
(ou zero
), nous voulons
nous lisons toujours 0 à partir de cet opérande. Pour cela, nous allons utiliser
Classe generic::IntLiteralOperand<>
définie dans
literal_operand.h
Cet opérande permet de stocker une valeur littérale (par opposition à une simulation
valeur immédiate). Sinon, le schéma est identique: commencez par extraire la
valeur rs1/rs2 du mot d'instruction. Si elle est égale à zéro, renvoyez le littéral
opérande avec un paramètre de modèle 0. Sinon, renvoie un registre standard
Opérande source à l'aide de la fonction d'assistance, en utilisant l'alias "abi" comme opérande
son nom.
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 traitons les différents opérandes immédiats. Les valeurs immédiates sont
stocké 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
la fonction Extractor utilisée, et si le type de stockage est
non signé, selon le 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 cliquez ici.
Ce tutoriel est maintenant terminé. Nous espérons qu'elle vous a été utile.