Gli obiettivi di questo tutorial sono:
- Scopri come vengono utilizzate le funzioni semantiche per implementare la semantica dell'istruzione.
- Scoprire la relazione tra le funzioni semantiche e la descrizione del decoder ISA.
- Scrivere funzioni semantiche per le istruzioni RiscV RV32I.
- Testa il simulatore finale eseguendo un piccolo "Hello World" eseguibile.
Panoramica delle funzioni semantiche
Una funzione semantica in MPACT-Sim è una funzione che implementa l'operazione di un'istruzione in modo che i suoi effetti collaterali siano visibili nello stato simulato allo stesso modo in cui gli effetti collaterali dell'istruzione sono visibili hardware. Rappresentazione interna del simulatore di ogni istruzione decodificata contiene un elemento richiamabile utilizzato per richiamare la funzione semantica istruzioni.
Una funzione semantica ha la firma void(Instruction *)
, ovvero una
che prende un puntatore a un'istanza della classe Instruction
e
restituisce void
.
La classe Instruction
è definita in
instruction.h
Per scrivere funzioni semantiche, siamo particolarmente interessati
i vettori dell'interfaccia dell'operando di origine e di destinazione a cui si accede utilizzando
Chiamate ai metodi Source(int i)
e Destination(int i)
.
Di seguito sono mostrate le interfacce degli operando di origine e di destinazione:
// 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;
};
Il modo di base di scrivere una funzione semantica per un operando normale di 3
come un'istruzione add
a 32 bit è il seguente:
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();
}
Analizziamo gli elementi di questa funzione. Le due prime righe della
Il corpo della funzione legge dagli operandi di origine 0 e 1. La chiamata a AsUint32(0)
interpreta i dati sottostanti come un array uint32_t
e recupera lo 0°
. Ciò vale indipendentemente dal fatto che il registro o il valore sottostante
con o senza valore. Le dimensioni (in elementi) dell'operando di origine possono essere
ottenuto dal metodo operando di origine shape()
, che restituisce un vettore
contenente il numero di elementi in ogni dimensione. Questo metodo restituisce {1}
per uno scalare, {16}
per un vettore di 16 elementi e {4, 4}
per un array 4x4.
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
A questo punto, a un uint32_t
temporaneo denominato c
viene assegnato il valore a + b
.
Nella riga successiva potrebbero essere necessarie ulteriori spiegazioni:
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
Un DataBuffer è un oggetto conteggiato ai fini di riferimento utilizzato per archiviare i valori in
stato simulato, come i registri. È relativamente non digitato, sebbene abbia una
dimensioni in base all'oggetto
da cui è allocato. In questo caso, la dimensione è
sizeof(uint32_t)
. Questa istruzione alloca un nuovo buffer di dati dimensionato per
destinazione che è la destinazione di questo operando di destinazione, in questo caso una
Registro di numeri interi a 32 bit. DataBuffer viene inoltre inizializzato con
latenza dell'architettura per l'istruzione. Questo valore viene specificato durante l'istruzione
decodificare.
Nella riga successiva l'istanza del buffer viene trattata come un array di uint32_t
e
scrive il valore memorizzato in c
nell'0° elemento.
db->Set<uint32_t>(0, c);
Infine, l'ultima istruzione invia il buffer di dati al simulatore da utilizzare come nuovo valore dello stato della macchina target (in questo caso un registro) dopo latenza dell'istruzione impostata al momento della decodifica dell'istruzione vettore di operando di destinazione compilato.
Anche se si tratta di una funzione ragionevolmente breve, ha comunque un po' di boilerplate che diventa ripetitivo quando si implementa l'istruzione dopo le istruzioni. Inoltre, può oscurare la semantica effettiva dell'istruzione. Nell'ordine per semplificare ulteriormente la scrittura delle funzioni semantiche per la maggior parte delle istruzioni, esistono varie funzioni helper basate su modelli definite in instruction_helpers.h. Questi assistenti nascondono il codice boilerplate per istruzioni con uno, due o tre operandi di origine e un singolo operando di destinazione. Diamo un'occhiata a un paio funzioni helper dell'operando:
// 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);
}
Noterai che invece di utilizzare un'istruzione come la seguente:
uint32_t a = inst->Source(0)->AsUint32(0);
La funzione helper utilizza:
generic::GetInstructionSource<Argument>(instruction, 0);
GetInstructionSource
è una famiglia di funzioni helper basate su modelli che
vengono usati per fornire metodi di accesso basati su modelli all'origine dell'istruzione
operandi. Senza di essi, ogni funzione di aiuto delle istruzioni avrebbe
specializzato per ogni tipo per accedere all'operando di origine con
Funzione As<int type>()
. Puoi vedere le definizioni di questi modelli
in
instruction.h.
Come puoi vedere ci sono tre implementazioni, a seconda che l'origine
tipi di operando sono gli stessi della destinazione, indipendentemente dal fatto che la destinazione sia
diverse dalle fonti o se sono tutte diverse. Ogni versione
la funzione porta un puntatore all'istanza dell'istruzione, nonché una funzione
(incluse le funzioni lambda). Ciò significa che ora possiamo riscrivere add
funzione semantica riportata sopra:
void MyAddFunction(Instruction *inst) {
generic::BinaryOp<uint32_t>(inst,
[](uint32_t a, uint32_t b) { return a + b; });
}
Quando viene compilata con bazel build -c opt
e copts = ["-O3"]
nella build
dovrebbe essere completamente in linea senza overhead, dandoci informazioni
concisione senza penalità in termini di prestazioni.
Come accennato in precedenza, esistono le funzioni helper per scalari unari, binarie e ternari istruzioni ed equivalenti vettoriali. Servono anche come modelli utili per creare i tuoi aiutanti per istruzioni che non sono adatte a tutti gli schemi.
Build iniziale
Se non hai modificato la directory in riscv_semantic_functions
, procedi nel seguente modo:
per ora. Quindi crea il progetto come segue. Questa build dovrebbe riuscire.
$ bazel build :riscv32i
...<snip>...
Non vengono generati file, quindi questa è solo una prova assicurati che sia tutto in ordine.
Aggiungi istruzioni ALU di tre operando
Ora aggiungiamo le funzioni semantiche per alcune ALU generiche a tre operandi
istruzioni. Apri il file rv32i_instructions.cc
e assicurati che qualsiasi
le definizioni mancanti vengono aggiunte al file rv32i_instructions.h
man mano che procedi.
Ecco le istruzioni che aggiungeremo:
add
: aggiunta di numeri interi a 32 bit.and
: bit a 32 bit e.or
: bit per bit a 32 bit o.sll
: spostamento logico a 32 bit verso sinistra.sltu
: set non firmato a 32 bit minore di.sra
: spostamento destro aritmetico a 32 bit.srl
: spostamento logico a destra a 32 bit.sub
: sottrarre il numero intero a 32 bit.xor
: xor a 32 bit per bit.
Se hai svolto i tutorial precedenti, ricorderai che abbiamo distinto tra le istruzioni registry-register e le istruzioni registry-immediate in il decoder. Quando si tratta di funzioni semantiche, non è più necessario farlo. Le interfacce degli operando leggono il valore dell'operando da qualsiasi registro o immediato, con la funzione semantica completamente indipendente sull'operando dell'origine sottostante.
Fatta eccezione per sra
, tutte le istruzioni riportate sopra possono essere considerate operative su
Valori non firmati a 32 bit, quindi per questi valori possiamo usare la funzione modello BinaryOp
che abbiamo esaminato in precedenza solo con un singolo argomento tipo di modello. Compila il campo
della funzione in rv32i_instructions.cc
di conseguenza. Tieni presente che solo i 5
bit del secondo operando alle istruzioni di spostamento vengono utilizzati per lo spostamento
importo. In caso contrario, tutte le operazioni sono nel formato 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
Per sra
useremo il modello BinaryOp
con tre argomenti. Esaminando
, il primo argomento di tipo è il tipo di risultato uint32_t
. Il secondo è
il tipo di operando di origine 0, in questo caso int32_t
, e l'ultimo è il tipo
dell'operando di origine 1, in questo caso uint32_t
. Questo rende il corpo di sra
funzione semantica:
generic::BinaryOp<uint32_t, int32_t, uint32_t>(
instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });
Apporta le modifiche e crea. Puoi verificare il tuo lavoro rispetto a rv32i_instructions.cc.
Aggiungi istruzioni ALU di due operando
Esistono solo due istruzioni ALU con due operandi: lui
e auipc
. Il primo
copia l'operando di origine pre-spostato direttamente nella destinazione. Quest'ultimo
aggiunge l'indirizzo dell'istruzione nell'immediato prima di scriverlo
destinazione. L'indirizzo dell'istruzione è accessibile dal metodo address()
dell'oggetto Instruction
.
Poiché esiste un solo operando di origine, non possiamo usare BinaryOp
.
dobbiamo usare UnaryOp
. Poiché possiamo trattare sia l'origine che
operandi di destinazione come uint32_t
possiamo usare il modello di argomento singolo
completamente gestita.
// 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);
}
Il corpo della funzione semantica per lui
è abbastanza banale,
che restituisce solo l'origine. La funzione semantica per auipc
introduce un
perché devi accedere al metodo address()
in Instruction
in esecuzione in un'istanza Compute Engine. La risposta è aggiungere instruction
all'acquisizione Lambda, in modo che
disponibile per l'uso nel corpo della funzione lambda. Invece di [](uint32_t a) { ...
}
come prima, la funzione lambda deve essere scritta [instruction](uint32_t a) { ... }
.
Ora è possibile utilizzare instruction
nel corpo lambda.
Apporta le modifiche e crea. Puoi verificare il tuo lavoro rispetto a rv32i_instructions.cc.
Aggiungi istruzioni per la modifica del flusso di controllo
Le istruzioni per la modifica del flusso di controllo da implementare sono suddivise in istruzioni di ramo condizionali (rami più brevi eseguiti se un confronto è valido) e le istruzioni jump-and-link, che consentono di implementare le chiamate di funzione (il tag -and-link viene rimosso impostando il link registrare a zero, in modo che le scritture non siano operative).
Aggiungi istruzioni per i ramo condizionali
Non esiste una funzione helper per l'istruzione ramo, quindi sono disponibili due opzioni. Scrivere le funzioni semantiche da zero o scrivere una funzione helper locale. Dato che dobbiamo implementare sei istruzioni di ramo, quest'ultimo sembra valga la pena impegno. Prima di farlo, diamo un'occhiata all'implementazione di un ramo funzione semantica dell'istruzione da zero.
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();
}
}
L'unica cosa che varia da un'istruzione all'altra è il ramo
e i tipi di dati (Int a 32 bit con o senza firma),
operandi di origine. Ciò significa che dobbiamo avere un parametro di modello per
operandi di origine. La funzione helper stessa deve utilizzare Instruction
e un oggetto richiamabile come std::function
che restituisce bool
come parametri. La funzione helper dovrebbe avere il seguente aspetto:
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();
}
}
Ora possiamo scrivere la funzione semantica bge
(ramo firmato maggiore o uguale)
come:
void RV32IBge(Instruction *instruction) {
BranchConditional<int32_t>(instruction,
[](int32_t a, int32_t b) { return a >= b; });
}
Le altre istruzioni per il ramo sono le seguenti:
- Beq: ramo uguale.
- Bgeu: ramo maggiore o uguale (non firmato).
- Blt: ramo minore di (firmato).
- Bltu: ramo minore di (non firmato).
- Bne: ramo diverso.
Apporta le modifiche necessarie per implementare queste funzioni semantiche e ricreare. Puoi verificare il tuo lavoro rispetto a rv32i_instructions.cc.
Aggiungi istruzioni di collegamento e collegamento
Non ha senso scrivere una funzione helper per il salto e il link istruzioni, perciò dovremo scriverle partendo da zero. Iniziamo con esaminando la semantica delle istruzioni.
L'istruzione jal
prende un offset dall'operando di origine 0 e lo aggiunge alla
pc (indirizzo di istruzioni) attuale per calcolare la destinazione del salto. Il target jumping
viene scritto nell'operando di destinazione 0. L'indirizzo di restituzione è l'indirizzo del
l'istruzione sequenziale successiva. Può essere calcolato aggiungendo il valore attuale
dimensione dell'istruzione al suo indirizzo. L'indirizzo di restituzione viene scritto
l'operando di destinazione 1. Ricordati di includere il puntatore dell'oggetto di istruzione
la cattura di Lambda.
L'istruzione jalr
prende un registro di base come operando di origine 0 e un offset
come operando di origine 1 e li somma per calcolare la destinazione del salto.
In caso contrario, è identico all'istruzione jal
.
In base a queste descrizioni della semantica dell'istruzione, scrivi le due risposte e build. Puoi verificare il tuo lavoro rispetto a rv32i_instructions.cc.
Aggiungi istruzioni per l'archivio di memoria
È necessario implementare tre istruzioni per lo store: il byte di archiviazione
(sb
), memorizza la mezza parola (sh
) e memorizza la parola (sw
). Istruzioni per il negozio
da quelle che abbiamo implementato finora, in quanto le istruzioni
in scrittura nello stato del processore locale. ma scrivono su una risorsa di sistema
memoria principale. MPACT-Sim non tratta la memoria come un operando di istruzione,
quindi l'accesso alla memoria deve essere eseguito utilizzando un'altra metodologia.
La soluzione è aggiungere metodi di accesso alla memoria all'oggetto MPACT-Sim ArchState
,
o, più correttamente, crea un nuovo oggetto di stato RiscV derivato da ArchState
in cui è possibile aggiungerlo. L'oggetto ArchState
gestisce le risorse principali, ad esempio
registri e altri oggetti di stato. Gestisce anche le linee di ritardo utilizzate
bufferizza i buffer di dati degli operando di destinazione finché non possono essere riscritti
gli oggetti del registro. La maggior parte delle istruzioni può essere implementata all'insaputa
questa classe, ma alcune, come le operazioni di memoria e altre risorse
istruzioni richiedono la funzionalità per risiedere in questo oggetto di stato.
Diamo un'occhiata alla funzione semantica dell'istruzione fence
,
già implementata in rv32i_instructions.cc
, ad esempio. fence
l'istruzione contiene il problema dell'istruzione fino a quando
completata. Viene utilizzato per garantire l'ordinamento della memoria tra le istruzioni
eseguite prima dell'istruzione e successive.
// 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 parte principale della funzione semantica dell'istruzione fence
sono le ultime due
linee. Innanzitutto, l'oggetto di stato viene recuperato utilizzando un metodo in Instruction
e downcast<>
alla classe derivata specifica RiscV. Fence
della classe RiscVState
viene chiamato per eseguire l'operazione di recinto.
Le istruzioni per il negozio funzionano allo stesso modo. Innanzitutto, l'indirizzo effettivo del
l'accesso alla memoria viene calcolato dagli operandi di origine dell'istruzione di base e offset,
il valore da archiviare viene recuperato dall'operando di origine successivo. Poi,
Si ottiene mediante la chiamata al metodo state()
e
static_cast<>
e viene chiamato il metodo appropriato.
Il metodo dell'oggetto RiscVState
StoreMemory
è relativamente semplice, ma presenta una
alcune implicazioni da tenere presenti:
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
Come abbiamo visto, il metodo prende tre parametri: il puntatore
l'istruzione stessa, l'indirizzo del negozio e un puntatore a un DataBuffer
contenente i dati del datastore. Tieni presente che non sono richieste dimensioni
L'istanza DataBuffer
stessa contiene un metodo size()
. Tuttavia, non c'è
l'operando di destinazione accessibile all'istruzione e che può essere utilizzato
alloca un'istanza DataBuffer
delle dimensioni appropriate. Dobbiamo invece
utilizza una fabbrica DataBuffer
ottenuta dal metodo db_factory()
in
l'istanza Instruction
. Il campo di fabbrica utilizza un metodo Allocate(int size)
che restituisce un'istanza DataBuffer
delle dimensioni richieste. Ecco un esempio
di come utilizzarlo per allocare un'istanza DataBuffer
per un archivio di mezza parola
(nota che auto
è una funzionalità C++ che deduce il tipo dal lato destro
lato del compito):
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(uint16_t));
Una volta ottenuta l'istanza DataBuffer
, possiamo scrivere come al solito:
db->Set<uint16_t>(0, value);
Quindi passalo all'interfaccia del datastore:
state->StoreMemory(instruction, address, db);
Non abbiamo ancora finito. Il riferimento viene conteggiato per l'istanza DataBuffer
. Questo
viene normalmente compreso e gestito con il metodo Submit
, in modo da mantenere il
il più semplice possibile. Tuttavia, StoreMemory
non è
scritto in questo modo. IncRef
l'istanza DataBuffer
mentre è in funzione
e DecRef
al termine dell'operazione. Tuttavia, se la funzione semantica non
DecRef
il suo riferimento, non verrà mai rivendicato. Di conseguenza, l'ultima riga
in modo che sia:
db->DecRef();
Ci sono tre funzioni di negozio e l'unica cosa a differenza è la dimensione
l'accesso alla memoria. Mi sembra una grande opportunità per un'altra persona
la funzione helper basata su modelli. L'unica differenza nella funzione store è
il tipo di valore del negozio, quindi il modello deve avere questo valore come argomento.
A parte questo, solo l'istanza Instruction
deve essere passata:
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();
}
Completa le funzioni semantiche del datastore e crea. Puoi controllare lavorare contro rv32i_instructions.cc.
Aggiungi istruzioni per il caricamento della memoria
Le istruzioni di caricamento da implementare sono le seguenti:
lb
: byte di caricamento, estensione del segno in una parola.lbu
: carica byte non firmati, zero-extend in una parola.lh
- carica la metà della parola, firma e amplia in una parola.lhu
: carica la metà della parola senza firma, l'estensione zero in una parola.lw
: carica parola.
Le istruzioni di caricamento sono le più complesse che abbiamo bisogno di modellare
questo tutorial. Sono simili alle istruzioni del negozio, in quanto devono
accedere all'oggetto RiscVState
, ma aggiunge complessità in ogni caricamento
istruzioni è divisa in due funzioni semantiche separate. La prima è
simile all'istruzione store, in quanto calcola l'indirizzo effettivo
e avvia l'accesso alla memoria. Il secondo viene eseguito quando
viene completato e scrive i dati della memoria nella destinazione del registro
operando.
Iniziamo esaminando la dichiarazione del metodo LoadMemory
in RiscVState
:
void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
Instruction *child_inst, ReferenceCount *context);
Rispetto al metodo StoreMemory
, LoadMemory
richiede due volte in più
: un puntatore a un'istanza Instruction
e un puntatore a un
riferimento contato context
oggetto. La prima è l'istruzione child che
implementa il write-back del registro (descritto nel tutorial sul decoder ISA). it
si accede utilizzando il metodo child()
nell'istanza Instruction
corrente.
Quest'ultimo è un puntatore a un'istanza di una classe che deriva
ReferenceCount
che in questo caso archivia un'istanza DataBuffer
che
che contengono i dati caricati. L'oggetto di contesto è disponibile tramite
Metodo context()
nell'oggetto Instruction
(anche se per la maggior parte delle istruzioni
il valore è impostato su nullptr
).
L'oggetto di contesto per i caricamenti di memoria RiscV è definito come il seguente struct:
// 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;
};
Le istruzioni per il caricamento sono le stesse, ad eccezione della dimensione dei dati (byte, mezza parola e parola) e indica se il valore caricato è esteso o meno. La Quest'ultimo tiene conto solo dell'istruzione child. Creiamo un modello la funzione helper per le istruzioni relative al caricamento principale. Sarà molto simile di archiviare un'istruzione, ma non accederà a un operando di origine per ottenere un valore, e verrà creato un oggetto di contesto.
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();
}
Come puoi vedere, la differenza principale è che l'istanza DataBuffer
allocata
vengono sia passate alla chiamata LoadMemory
come parametro, sia memorizzate nel
LoadContext
oggetto.
Le funzioni semantiche dell'istruzione child sono tutte molto simili. In primo luogo,
LoadContext
si ottiene chiamando il metodo Instruction
context()
e
trasmesso in modo statico sul LoadContext *
. Secondo, il valore (in base ai dati
) vengono lette dall'istanza DataBuffer
con i dati di caricamento. In terzo luogo, un nuovo
L'istanza DataBuffer
è allocata dall'operando di destinazione. Infine,
il valore caricato viene scritto nella nuova istanza DataBuffer
e viene inserito Submit
.
Anche in questo caso, una funzione helper basata su modelli è una buona idea:
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();
}
Implementa le ultime funzioni helper e le funzioni semantiche. Paga al tipo di dati che utilizzi nel modello per ogni funzione helper e che corrisponda alle dimensioni e alla natura firmata/non firmata del caricamento istruzioni.
Puoi verificare il tuo lavoro rispetto a rv32i_instructions.cc.
Crea ed esegui il simulatore finale
Ora che abbiamo fatto tutte le domande difficili, possiamo costruire il simulatore finale. La
librerie C++ di primo livello che collegano insieme tutto il lavoro di questi tutorial sono
si trova in other/
. Non è necessario esaminare troppo attentamente quel codice. Me
visiterai quell'argomento in un tutorial avanzato futuro.
Cambia la directory di lavoro in other/
e crea. Deve creare senza
errori.
$ cd ../other
$ bazel build :rv32i_sim
Nella directory c'è un semplice "Hello World" nel file
hello_rv32i.elf
. Per eseguire il simulatore su questo file e vedere i risultati:
$ bazel run :rv32i_sim -- other/hello_rv32i.elf
I risultati dovrebbero essere simili ai seguenti:
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
$
Il simulatore può essere eseguito anche in modalità interattiva utilizzando il comando bazel
run :rv32i_sim -- -i other/hello_rv32i.elf
. Viene visualizzata una semplice
shell dei comandi. Digita help
al prompt per visualizzare i comandi disponibili.
$ 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] >
Il tutorial termina qui. Ci auguriamo che sia stato utile.