Les objectifs de ce tutoriel sont les suivants:
- Découvrez comment les fonctions sémantiques sont utilisées pour implémenter la sémantique des instructions.
- Découvrez le lien entre les fonctions sémantiques et la description du décodeur ISA.
- Écrire les fonctions sémantiques d'instructions pour les instructions RiscV RV32I.
- Testez le simulateur final en exécutant un petit "Hello World" exécutable.
Présentation des fonctions sémantiques
Dans MPACT-Sim, une fonction sémantique met en œuvre l'opération d'une instruction afin que ses effets secondaires soient visibles à l'état simulé de la même manière que les effets secondaires de l'instruction sont visibles lorsqu'ils sont exécutés dans matériel. Représentation interne de chaque instruction décodée par le simulateur contient un appelable qui est utilisé pour appeler la fonction sémantique correspondant à instruction.
Une fonction sémantique a la signature void(Instruction *)
, c'est-à-dire une
qui accepte un pointeur vers une instance de la classe Instruction
et
renvoie void
.
La classe Instruction
est définie dans
instruction.h
Pour écrire des fonctions sémantiques, nous nous intéressons
les vecteurs d'interface d'opérande source et de destination auxquels vous accédez à l'aide de la méthode
Appels de méthode Source(int i)
et Destination(int i)
.
Les interfaces d'opérande source et de destination sont présentées ci-dessous:
// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
public:
// Methods for accessing the nth value element.
virtual bool AsBool(int index) = 0;
virtual int8_t AsInt8(int index) = 0;
virtual uint8_t AsUint8(int index) = 0;
virtual int16_t AsInt16(int index) = 0;
virtual uint16_t AsUint16(int) = 0;
virtual int32_t AsInt32(int index) = 0;
virtual uint32_t AsUint32(int index) = 0;
virtual int64_t AsInt64(int index) = 0;
virtual uint64_t AsUint64(int index) = 0;
// Return a pointer to the object instance that implements the state in
// question (or nullptr) if no such object "makes sense". This is used if
// the object requires additional manipulation - such as a fifo that needs
// to be pop'ed. If no such manipulation is required, nullptr should be
// returned.
virtual std::any GetObject() const = 0;
// Return the shape of the operand (the number of elements in each dimension).
// For instance {1} indicates a scalar quantity, whereas {128} indicates an
// 128 element vector quantity.
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~SourceOperandInterface() = default;
};
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
public:
virtual ~DestinationOperandInterface() = default;
// Allocates a data buffer with ownership, latency and delay line set up.
virtual DataBuffer *AllocateDataBuffer() = 0;
// Takes an existing data buffer, and initializes it for the destination
// as if AllocateDataBuffer had been called.
virtual void InitializeDataBuffer(DataBuffer *db) = 0;
// Allocates and initializes data buffer as if AllocateDataBuffer had been
// called, but also copies in the value from the current value of the
// destination.
virtual DataBuffer *CopyDataBuffer() = 0;
// Returns the latency associated with the destination operand.
virtual int latency() const = 0;
// Return a pointer to the object instance that implmements the state in
// question (or nullptr if no such object "makes sense").
virtual std::any GetObject() const = 0;
// Returns the order of the destination operand (size in each dimension).
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
};
La méthode de base pour écrire une fonction sémantique pour un opérande 3 normal
d'une instruction telle qu'une instruction add
32 bits est la suivante:
void MyAddFunction(Instruction *inst) {
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
uint32_t c = a + b;
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, c);
db->Submit();
}
Décomposons les éléments de cette fonction. Les deux premières lignes du
le corps de la fonction lit les opérandes sources 0 et 1. L'appel AsUint32(0)
interprète les données sous-jacentes comme un tableau uint32_t
et extrait la valeur 0
. Cela est vrai que le registre ou la valeur sous-jacents
avec ou sans valeur dans le tableau. La taille (en éléments) de l'opérande source peut être de
obtenu à partir de la méthode d'opérande source shape()
, qui renvoie un vecteur
contenant le nombre d'éléments dans chaque dimension. Cette méthode renvoie {1}
pour une valeur scalaire, {16}
pour un vecteur à 16 éléments et {4, 4}
pour un tableau 4x4.
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
Ensuite, un uint32_t
temporaire nommé c
se voit attribuer la valeur a + b
.
La ligne suivante peut nécessiter quelques explications supplémentaires:
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
Un DataBuffer est un objet compté de référence qui est utilisé pour stocker des valeurs dans
des états simulés tels que des registres. Il n'est pas typé, bien qu'il comporte une
en fonction de l'objet auquel elle est allouée. Dans ce cas, cette taille est
sizeof(uint32_t)
Cette instruction alloue un nouveau tampon de données de taille
destination qui est la cible de cet opérande de destination. Dans le cas présent,
Registre d'entiers 32 bits. Le DataBuffer est également initialisé avec
la latence de l'architecture pour l'instruction. Cette valeur est spécifiée lors de l'instruction
le décodage.
La ligne suivante traite l'instance de tampon de données comme un tableau de uint32_t
et
écrit la valeur stockée dans c
dans l'élément 0.
db->Set<uint32_t>(0, c);
Enfin, la dernière instruction envoie le tampon de données au simulateur pour qu'il soit utilisé comme nouvelle valeur de l'état de la machine cible (dans ce cas, un registre) après la la latence de l'instruction définie lors du décodage de l'instruction et vecteur d'opérande de destination renseigné.
Bien qu'il s'agisse d'une fonction relativement brève, elle comporte un peu de code récurrent qui devient répétitif lors de l'implémentation d'instructions à la suite de l'instruction. De plus, cela peut masquer la sémantique réelle de l'instruction. Dans la commande afin de simplifier davantage l'écriture des fonctions sémantiques pour la plupart des instructions, un certain nombre de fonctions helper modélisées sont définies dans instruction_helpers.h : Ces assistants masquent le code récurrent des instructions avec un, deux ou trois des opérandes sources et un seul opérande de destination. Jetons un coup d’œil à deux fonctions d'assistance d'opérande:
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument1, Argument2)> operation) {
Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument, Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Result, Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
Vous remarquerez qu'au lieu d'utiliser une instruction comme:
uint32_t a = inst->Source(0)->AsUint32(0);
La fonction d'assistance utilise:
generic::GetInstructionSource<Argument>(instruction, 0);
GetInstructionSource
est une famille de fonctions d'assistance modélisées qui
sont utilisés pour fournir des méthodes d'accès modélisées à la source d'instructions
opérandes. Sans elles, chacune des fonctions
d'assistance des instructions aurait
"spécialisé pour chaque type" pour accéder à l'opérande source contenant
fonction As<int type>()
. Vous pouvez consulter les définitions de ces modèles
fonctions dans
instruction.h.
Comme vous pouvez le voir, il existe trois implémentations, selon que la source
les types d'opérande sont identiques à la destination, que celle-ci soit
différentes des sources, ou
si elles sont toutes différentes. Chaque version de
La fonction utilise un pointeur vers l'instance d'instruction
(qui incluent les fonctions lambda). Cela signifie que nous pouvons désormais réécrire add
fonction sémantique ci-dessus, comme suit:
void MyAddFunction(Instruction *inst) {
generic::BinaryOp<uint32_t>(inst,
[](uint32_t a, uint32_t b) { return a + b; });
}
Lorsqu'il est compilé avec bazel build -c opt
et copts = ["-O3"]
dans le build
celui-ci doit s'aligner entièrement et ne pas avoir de frais généraux, ce qui nous donne
succincte sans nuire aux performances.
Comme indiqué précédemment, il existe des fonctions d'assistance pour les valeurs scalaires unaires, binaires et ternaires. d'instructions et d'équivalents vectoriels. Ils servent aussi de modèles utiles pour créer vos propres assistants pour des instructions qui ne correspondent pas au modèle général.
Compilation initiale
Si vous n'avez pas remplacé le répertoire par riscv_semantic_functions
, faites-le
dès maintenant. Compilez ensuite le projet comme suit. Cette compilation devrait aboutir.
$ bazel build :riscv32i
...<snip>...
Aucun fichier n'est généré. Il s'agit donc d'une simulation vous assurer que tout est en ordre.
Ajouter trois instructions ALU d'opérande
Ajoutons maintenant les fonctions sémantiques pour une ALU générique à 3 opérandes.
instructions. Ouvrez le fichier rv32i_instructions.cc
et assurez-vous que les
les définitions manquantes sont ajoutées au fichier rv32i_instructions.h
au fur et à mesure.
Les instructions que nous allons ajouter sont les suivantes:
add
: nombre total de nombres entiers 32 bits additionnel.and
: et 32 bits au niveau du bit.or
: ou 32 bits au niveau du bit.sll
: décalage logique de 32 bits vers la gauche.sltu
: ensemble 32 bits non signé inférieur à.sra
: décalage arithmétique vers la droite 32 bits.srl
: décalage logique vers la droite sur 32 bits.sub
: soustraction d'un entier de 32 bits.xor
: fonction XOR 32 bits.
Si vous avez suivi les tutoriels précédents, vous vous souvenez peut-être que nous avons distingué entre les instructions d'enregistrement et d'enregistrement et les instructions d'enregistrement immédiat dans le décodeur. Nous n'avons plus besoin de faire cela pour les fonctions sémantiques. Les interfaces d'opérande lisent la valeur de l'opérande à partir de n'importe quel opérande est, registre ou immédiat, avec la fonction sémantique complètement agnostique par rapport à ce qu'est vraiment l'opérande source sous-jacent.
À l'exception de sra
, toutes les instructions ci-dessus peuvent être considérées comme fonctionnant sur
Valeurs non signées de 32 bits. Pour celles-ci, nous pouvons utiliser la fonction de modèle BinaryOp
.
que nous avons vu précédemment avec
un seul argument de type de modèle. Remplissez
corps de fonction dans rv32i_instructions.cc
en conséquence. Notez que seuls les 5 premiers chiffres
bits du deuxième opérande vers les instructions
shift sont utilisés pour le décalage
montant. Sinon, toutes les opérations se présentent sous la forme src0 op src1
:
add
:a + b
and
:a & b
or
:a | b
sll
:a << (b & 0x1f)
sltu
:(a < b) ? 1 : 0
srl
:a >> (b & 0x1f)
sub
:a - b
xor
:a ^ b
Pour sra
, nous utiliserons le modèle à trois arguments BinaryOp
. En consultant
modèle, le premier argument "type" est le type de résultat uint32_t
. Le deuxième est
le type d'opérande source 0, ici int32_t
, le dernier étant le type
de l'opérande source 1, dans ce cas uint32_t
. Ainsi, le corps de sra
fonction sémantique suivante:
generic::BinaryOp<uint32_t, int32_t, uint32_t>(
instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });
Apportez les modifications nécessaires et continuez. Vous pouvez comparer votre travail par rapport rv32i_instructions.cc.
Ajouter deux instructions ALU d'opérande
Il n'y a que deux instructions ALU à deux opérandes: lui
et auipc
. L'ancienne
copie l'opérande source pré-décalé directement dans la destination. La seconde
ajoute l'adresse d'instruction à la valeur "immédiate" avant de l'écrire dans le
vers votre destination. L'adresse de l'instruction est accessible à partir de la méthode address()
.
de l'objet Instruction
.
Puisqu'il n'y a qu'un seul opérande source, nous ne pouvons pas utiliser BinaryOp
à la place.
nous devons utiliser UnaryOp
. Puisque nous pouvons traiter à la fois la source et
opérandes de destination comme uint32_t
, nous pouvons utiliser le modèle à argument unique
version.
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
Le corps de la fonction sémantique de lui
est aussi simple que possible.
il suffit de renvoyer la source. La fonction sémantique de auipc
introduit un élément mineur
car vous devez accéder à la méthode address()
dans Instruction
Compute Engine. La solution consiste à ajouter instruction
à la capture lambda, ce qui
disponibles dans le corps de la fonction lambda. Au lieu de [](uint32_t a) { ...
}
comme précédemment, le lambda doit être écrit [instruction](uint32_t a) { ... }
.
instruction
peut maintenant être utilisé dans le corps du lambda.
Apportez les modifications nécessaires et continuez. Vous pouvez comparer votre travail par rapport rv32i_instructions.cc.
Ajouter des instructions pour modifier le flux de contrôle
Les instructions de modification du flux de contrôle que vous devez implémenter sont réparties en deux parties : en instructions de branche conditionnelles (branches plus courtes exécutées si une comparaison est vraie) et les instructions de renvoi et de lien, qui sont utilisées pour implémenter les appels de fonction (l'élément -and-link est supprimé en définissant le lien à zéro, ce qui rend ces écritures no-ops).
Ajouter des instructions pour les branches conditionnelles
Il n'existe pas de fonction d'assistance pour les instructions de branchement. Il existe donc deux options. Écrivez les fonctions sémantiques à partir de zéro ou écrivez une fonction d'assistance locale. Étant donné que nous devons implémenter six instructions pour les branches, la dernière semble en valoir la peine d'efforts. Avant cela, examinons l'implémentation d'une branche la fonction sémantique d'instruction en partant de zéro.
void MyConditionalBranchGreaterEqual(Instruction *instruction) {
int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
if (a >= b) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0,m target);
db->Submit();
}
}
La seule chose qui varie selon les instructions sur la branche est la branche
et les types de données, int 32 bits signés et non signés, des deux
les opérandes sources. Cela signifie que nous devons disposer d'un paramètre de modèle
les opérandes sources. La fonction d'assistance elle-même doit prendre la Instruction
et un objet appelable comme std::function
qui renvoie bool
comme paramètres. La fonction d'assistance ressemblerait à ceci:
template <typename OperandType>
static inline void BranchConditional(
Instruction *instruction,
std::function<bool(OperandType, OperandType)> cond) {
OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
if (cond(a, b)) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, target);
db->Submit();
}
}
Nous pouvons maintenant écrire la fonction sémantique bge
(branche signée supérieure ou égale).
en tant que:
void RV32IBge(Instruction *instruction) {
BranchConditional<int32_t>(instruction,
[](int32_t a, int32_t b) { return a >= b; });
}
Les instructions pour les branches restantes sont les suivantes:
- Beq - branch equal.
- Bgeu - Branche supérieure ou égale (non signée)
- BLT - Branche inférieure à (signée).
- Bltu : branche inférieure à (non signée).
- Bne - branche différente.
Apportez les modifications nécessaires pour implémenter ces fonctions sémantiques. recompiler. Vous pouvez comparer votre travail par rapport rv32i_instructions.cc.
Ajouter des instructions de renvoi et de lien
Il n'y a aucun intérêt à écrire une fonction d'assistance pour le saut et le lien. des instructions, nous devrons donc les écrire à partir de zéro. Commençons par en examinant la sémantique de leurs instructions.
L'instruction jal
extrait un décalage de l'opérande source 0 et l'ajoute à la
pc actuelle (adresse d'instruction) pour calculer la cible du saut. La cible du saut
est écrite dans l'opérande
de destination 0. L'adresse de retour est celle
l'instruction séquentielle suivante. Elle peut être calculée en ajoutant la valeur
la taille de l'instruction à son adresse. L'adresse de retour est écrite dans
Opérande de destination 1. N'oubliez pas d'inclure le pointeur d'objet d'instruction
la capture lambda.
L'instruction jalr
utilise un registre de base en tant qu'opérande source 0 et un décalage
en tant qu'opérande source 1 et les additionne pour calculer la cible de saut.
Dans le cas contraire, elle est identique à l'instruction jal
.
Sur la base de ces descriptions de la sémantique d'instruction, rédigez les deux les fonctions et la compilation. Vous pouvez comparer votre travail par rapport rv32i_instructions.cc.
Ajouter des instructions concernant le stockage en mémoire
Il y a trois instructions de magasin que nous devons implémenter: store byte
(sb
), stocker le mot (sh
) et le mot (sw
). Instructions pour les magasins
diffèrent des instructions que nous avons implémentées jusqu'à présent, car elles ne
en écriture dans l'état local du processeur. À la place, ils écrivent dans une ressource système :
mémoire principale. MPACT-Sim ne traite pas la mémoire
comme un opérande d'instruction,
donc l'accès à la mémoire doit être effectué
en utilisant une autre méthodologie.
La solution consiste à ajouter des méthodes d'accès à la mémoire à l'objet ArchState
MPACT-Sim,
ou plus exactement, créez un objet d'état RiscV dérivé de ArchState
.
où cela peut être ajouté. L'objet ArchState
gère les ressources principales, telles que
des registres et d'autres objets d'état. Il gère également les lignes
à retard utilisées pour
les tampons de données de l'opérande de destination jusqu'à ce qu'ils puissent être réécrits
les objets registre. La plupart des instructions peuvent être mises en œuvre sans connaître
dans cette classe, mais d'autres, comme les opérations de mémoire et d'autres
les instructions nécessitent que la fonctionnalité se trouve dans cet objet d'état.
Examinons la fonction sémantique de l'instruction fence
, qui est
déjà implémentée dans rv32i_instructions.cc
à titre d'exemple. fence
contient le problème jusqu'à ce que certaines opérations de mémoire
terminé. Il est utilisé pour garantir l'ordre de la mémoire entre les instructions
qui s'exécutent avant l'instruction
et celles qui s'exécutent après.
// Fence.
void RV32IFence(Instruction *instruction) {
uint32_t bits = instruction->Source(0)->AsUint32(0);
int fm = (bits >> 8) & 0xf;
int predecessor = (bits >> 4) & 0xf;
int successor = bits & 0xf;
auto *state = static_cast<RiscVState *>(instruction->state());
state->Fence(instruction, fm, predecessor, successor);
}
La partie clé de la fonction sémantique de l'instruction fence
correspond aux deux derniers
lignes. L'objet d'état est d'abord récupéré à l'aide d'une méthode dans Instruction
.
et downcast<>
à la classe dérivée spécifique de RiscV. Ensuite, le Fence
de la classe RiscVState
est appelée pour effectuer l'opération de cloisonnement.
Les instructions concernant le magasin fonctionneront de la même manière. Tout d'abord, l'adresse effective du
l'accès à la mémoire est calculé à partir des opérandes sources de l'instruction de base et de décalage,
alors la valeur à stocker est extraite de l'opérande source suivant. Ensuite,
L'objet d'état RiscV est obtenu via l'appel de la méthode state()
et
static_cast<>
, et la méthode appropriée est appelée.
La méthode StoreMemory
de l'objet RiscVState
est relativement simple, mais présente une
deux conséquences que nous devons connaître:
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
Comme vous pouvez le voir, la méthode utilise trois paramètres : le pointeur vers le magasin
l'instruction elle-même, l'adresse du magasin et un pointeur vers un DataBuffer
qui contient les données du magasin. Notez qu'aucune taille n'est requise,
L'instance DataBuffer
contient une méthode size()
. Toutefois, il n'est pas possible
opérande de destination accessible à l'instruction, qui peut être utilisé pour
allouez une instance DataBuffer
de la taille appropriée. Au lieu de cela, nous devons
utilisez une fabrique DataBuffer
obtenue à partir de la méthode db_factory()
dans
l'instance Instruction
. La fabrique dispose d'une méthode Allocate(int size)
qui renvoie une instance DataBuffer
de la taille requise. Voici un exemple :
de l'utilisation de cette valeur afin d'allouer une instance DataBuffer
à un magasin de demi-mots
Notez que auto
est une fonctionnalité C++ qui déduit le type à partir de la droite.
côté de la mission):
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(uint16_t));
Une fois que nous avons l'instance DataBuffer
, nous pouvons l'écrire comme d'habitude:
db->Set<uint16_t>(0, value);
Transmettez-le ensuite à l'interface du Memory Store:
state->StoreMemory(instruction, address, db);
Nous n'avons pas encore terminé. L'instance DataBuffer
est comptabilisée comme référence. Ce
est normalement compris et géré par la méthode Submit
, de manière à conserver
le cas d'utilisation le plus fréquent aussi simple que possible. Cependant, StoreMemory
n'est pas
écrit de cette façon. Elle IncRef
l'instance DataBuffer
pendant son fonctionnement
dessus, puis DecRef
une fois que vous avez terminé. Cependant, si la fonction sémantique
DecRef
sa propre référence, elle ne sera jamais récupérée. Ainsi, la dernière ligne a
doit être:
db->DecRef();
Il existe trois fonctions de magasin.
La seule différence est la taille
l'accès à la mémoire. Cela semble être une excellente opportunité pour une autre entreprise locale
à partir d'un modèle. La seule chose différente dans
la fonction Store est
le type de la valeur du magasin, le modèle doit donc l'utiliser en tant qu'argument.
En dehors de cela, seule l'instance Instruction
doit être transmise:
template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->Set<ValueType>(0, value);
state->StoreMemory(instruction, address, db);
db->DecRef();
}
Terminez la compilation des fonctions sémantiques du magasin. Vous pouvez consulter travailler contre rv32i_instructions.cc.
Ajouter les instructions de chargement de la mémoire
Les instructions de chargement à implémenter sont les suivantes:
lb
: octet de chargement, extension des signes dans un mot.lbu
: charge les octets non signés, qui sont remplacés par des valeurs nulles.lh
: chargement d'un demi-mot, extension des signes dans un mot.lhu
: charge un demi-mot non signé, qui se transforme en un mot.lw
- charger le mot.
Les instructions de chargement sont les instructions les plus complexes pour la modélisation
ce tutoriel. Elles sont semblables aux instructions
de stockage, dans la mesure où elles doivent
accéder à l'objet RiscVState
, mais cela complique le fait que chaque charge
les instructions est divisée en deux
fonctions sémantiques distinctes. Le premier est
semblable à l'instruction "Store", dans la mesure où elle calcule l'adresse effective
et lance l'accès à la mémoire. La seconde est exécutée lorsque la mémoire
l'accès est terminé, et écrit les données de mémoire dans le registre de destination
opérande.
Commençons par examiner la déclaration de la méthode LoadMemory
dans RiscVState
:
void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
Instruction *child_inst, ReferenceCount *context);
Par rapport à la méthode StoreMemory
, LoadMemory
prend deux autres
les paramètres: un pointeur vers une instance Instruction
et un pointeur vers un
référence a compté l'objet context
. La première est l'instruction child,
met en œuvre l'écriture différée du registre (décrit dans le tutoriel sur les décodeurs ISA). Il
est accessible à l'aide de la méthode child()
dans l'instance Instruction
actuelle.
Cette dernière pointe vers une instance d'une classe dérivée de
ReferenceCount
qui, dans ce cas, stocke une instance DataBuffer
qui, dans ce cas,
contenant les données chargées. L'objet de contexte est disponible via la
Méthode context()
dans l'objet Instruction
(bien que, pour la plupart des instructions,
définie sur nullptr
).
L'objet de contexte pour les chargements de mémoire RiscV est défini comme le struct suivant:
// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
~LoadContext() override {
if (value_db != nullptr) value_db->DecRef();
}
// Override the base class method so that the data buffer can be DecRef'ed
// when the context object is recycled.
void OnRefCountIsZero() override {
if (value_db != nullptr) value_db->DecRef();
value_db = nullptr;
// Call the base class method.
generic::ReferenceCount::OnRefCountIsZero();
}
// Data buffers for the value loaded from memory (byte, half, word, etc.).
DataBuffer *value_db = nullptr;
};
Les instructions de chargement sont les mêmes, sauf pour la taille des données (octets, un demi-mot et un mot) et détermine si la valeur chargée est étendue à un signe ou non. La Le second ne prend en compte que l'instruction child. Créons un modèle pour les instructions de chargement principales. Il est très similaire stocker une instruction, sauf qu'elle n'accède pas à un opérande source pour obtenir une valeur, pour créer un objet de contexte.
template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->set_latency(0);
auto *context = new riscv::LoadContext(db);
state->LoadMemory(instruction, address, db, instruction->child(), context);
context->DecRef();
}
Comme vous pouvez le voir, la principale différence est que l'instance DataBuffer
allouée
est à la fois transmis à l'appel LoadMemory
en tant que paramètre et stocké dans la
LoadContext
.
Les fonctions sémantiques de l'instruction child sont toutes très similaires. Tout d'abord, le
LoadContext
s'obtient en appelant la méthode Instruction
context()
.
transférés de manière statique vers le LoadContext *
. Deuxièmement, la valeur (selon les données
type) est lu à partir de l'instance DataBuffer
de chargement de données. Troisièmement,
L'instance DataBuffer
est allouée à partir de l'opérande de destination. Enfin, la fonction
la valeur chargée est écrite dans la nouvelle instance DataBuffer
et Submit
.
Encore une fois, une fonction d'assistance modélisée est une bonne idée:
template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
auto *context = down_cast<riscv::LoadContext *>(instruction->context());
uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, value);
db->Submit();
}
Implémentez ces dernières fonctions d'assistance et fonctions sémantiques. Payer attention au type de données que vous utilisez dans le modèle pour chaque fonction d'assistance et qu'il correspond à la taille et à la nature signée/non signée de la charge. instruction.
Vous pouvez comparer votre travail par rapport rv32i_instructions.cc.
Créer et exécuter le simulateur final
Maintenant que nous avons terminé, nous pouvons créer le simulateur final. La
des bibliothèques C++ de premier niveau qui associent tout le travail de ces tutoriels sont
situé dans le pays suivant : other/
. Il n'est pas nécessaire de regarder trop attentivement ce code. Mer
consulteront ce sujet dans un prochain didacticiel avancé.
Remplacez le répertoire de travail par other/
, puis compilez l'opération. Il doit se construire sans
les erreurs.
$ cd ../other
$ bazel build :rv32i_sim
Ce répertoire contient un simple "hello world" programme dans le fichier
hello_rv32i.elf
Pour exécuter le simulateur sur ce fichier et afficher les résultats:
$ bazel run :rv32i_sim -- other/hello_rv32i.elf
Le résultat doit ressembler à ceci:
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$
Vous pouvez également exécuter le simulateur en mode interactif à l'aide de la commande bazel
run :rv32i_sim -- -i other/hello_rv32i.elf
. Cela fait apparaître un
shell de commande. Saisissez help
lorsque vous y êtes invité pour afficher les commandes disponibles.
$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000 addi ra, 0, 0
[0] > help
quit - exit command shell.
core [N] - direct subsequent commands to core N
(default: 0).
run - run program from current pc until a
breakpoint or exit. Wait until halted.
run free - run program in background from current pc
until breakpoint or exit.
wait - wait for any free run to complete.
step [N] - step [N] instructions (default: 1).
halt - halt a running program.
reg get NAME [FORMAT] - get the value or register NAME.
reg NAME [FORMAT] - get the value of register NAME.
reg set NAME VALUE - set register NAME to VALUE.
reg set NAME SYMBOL - set register NAME to value of SYMBOL.
mem get VALUE [FORMAT] - get memory from location VALUE according to
format. The format is a letter (o, d, u, x,
or X) followed by width (8, 16, 32, 64).
The default format is x32.
mem get SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem set VALUE [FORMAT] VALUE - set memory at location VALUE(1) to VALUE(2)
according to FORMAT. Default format is x32.
mem set SYMBOL [FORMAT] VALUE - set memory at location SYMBOL to VALUE
according to FORMAT. Default format is x32.
break set VALUE - set breakpoint at address VALUE.
break set SYMBOL - set breakpoint at value of SYMBOL.
break VALUE - set breakpoint at address VALUE.
break SYMBOL - set breakpoint at value of SYMBOL.
break clear VALUE - clear breakpoint at address VALUE.
break clear SYMBOL - clear breakpoint at value of SYMBOL.
break clear all - remove all breakpoints.
help - display this message.
_start:
80000000 addi ra, 0, 0
[0] >
Ce tutoriel est maintenant terminé. Nous espérons que ces informations vous ont été utiles.