Anleitung zu semantischen Anweisungen für Funktionen

Mit dieser Anleitung werden folgende Ziele erreicht:

  • Anleitung zum Implementieren einer Anweisungssemantik mit semantischen Funktionen
  • Hier erfahren Sie, wie semantische Funktionen zur Beschreibung des ISA-Decodierers zusammenhängen.
  • Schreibe semantische Funktionen für RiscV RV32I-Anweisungen.
  • Fertigen Simulator testen, indem Sie ein kleines "Hello World"-Skript ausführen ausführbar sein.

Übersicht über semantische Funktionen

Eine semantische Funktion in MPACT-Sim ist eine Funktion, mit der die Operation implementiert wird. einer Anweisung so dargestellt, dass ihre Nebeneffekte im simulierten Zustand sichtbar sind auf die gleiche Weise, wie die Nebeneffekte der Anweisung sichtbar sind, wenn sie in Hardware. Die interne Darstellung der einzelnen decodierten Anweisungen im Simulator enthält ein Callable, mit dem die semantische Funktion für diese Anleitung.

Eine semantische Funktion hat die Signatur void(Instruction *), also eine -Funktion, die einen Zeiger auf eine Instanz der Instruction-Klasse gibt void zurück.

Die Klasse Instruction ist definiert in instruction.h

Im Zusammenhang mit dem Schreiben semantischen Funktionen sind insbesondere die Schnittstellenvektoren für Quell- und Zieloperanden, auf die mithilfe der Methode Aufrufe der Methoden Source(int i) und Destination(int i).

Nachfolgend sind die Schnittstellen für Quell- und Ziel-Operanden dargestellt:

// 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;
};

Die grundlegende Art, eine semantische Funktion für einen normalen 3-Operanden zu schreiben 32-Bit-add-Anweisung wie folgt:

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();
}

Schauen wir uns die einzelnen Bestandteile dieser Funktion an. Die beiden ersten Zeilen der Der Funktionstext liest aus den Quelloperanden 0 und 1. Der AsUint32(0)-Aufruf interpretiert die zugrunde liegenden Daten als uint32_t-Array und ruft den 0. -Elements. Dies gilt unabhängig davon, ob das zugrunde liegende Register oder der zugrunde liegende Wert Arraywert oder nicht. Die Größe (in Elementen) des Quelloperanden kann wird aus der Quelloperandenmethode shape() abgerufen, die einen Vektor zurückgibt der die Anzahl der Elemente in jeder Dimension enthält. Diese Methode gibt {1} zurück. für einen Skalar, {16} für einen Vektor mit 16 Elementen und {4, 4} für ein 4 × 4-Array.

  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);

Dann wird der temporären uint32_t mit dem Namen c der Wert a + b zugewiesen.

Für die nächste Zeile ist möglicherweise etwas mehr Erklärung erforderlich:

  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();

Ein DataBuffer ist ein gezähltes Referenzobjekt, das zum Speichern von Werten in simulierten Zustand wie Registern. Er ist relativ typisiert, hat jedoch einen Größe basierend auf dem Objekt, von dem es zugewiesen ist. In diesem Fall ist die Größe sizeof(uint32_t) Diese Anweisung weist einen neuen Datenpuffer in der Größe für die Das Ziel, das das Ziel dieses Zieloperanden ist – in diesem Fall ein 32-Bit-Ganzzahlregister. Der DataBuffer wird ebenfalls mit dem Parameter Architekturlatenz für den Anweisungen. Dies wird während der Anweisung angegeben. decodieren.

In der nächsten Zeile wird die Datenpufferinstanz als Array von uint32_t und schreibt den in c gespeicherten Wert in das 0. Element.

  db->Set<uint32_t>(0, c);

Mit der letzten Anweisung wird der Datenpuffer an den Simulator gesendet, der verwendet werden soll. als neuen Wert des Zielmaschinenstatus (in diesem Fall ein Register) nach dem Latenz des Befehls, der bei der Decodierung des Befehls festgelegt wurde, und der Zieloperandenvektor ausgefüllt.

Dies ist zwar eine relativ kurze Funktion, enthält aber einige Textbausteine. der sich bei der Implementierung einer Anweisung nach der Anweisung wiederholt. Außerdem kann dadurch die eigentliche Semantik der Anleitung verdeckt werden. Um um das Schreiben der semantischen Funktionen für die meisten Anweisungen zu vereinfachen, sind eine Reihe von helper-Funktionen in instruction_helpers.h Diese Helfer verbergen den Boilerplate-Code für Anweisungen mit ein, zwei oder drei Quelloperanden und einen einzelnen Zieloperanden. Werfen wir einen Blick auf zwei Operand-Hilfsfunktionen:

// 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);
}

Sie werden feststellen, dass statt einer Anweisung wie

  uint32_t a = inst->Source(0)->AsUint32(0);

Die Hilfsfunktion verwendet:

generic::GetInstructionSource<Argument>(instruction, 0);

GetInstructionSource ist eine Familie von vorlagenbasierten Hilfsfunktionen, die werden verwendet, um vorlagenbasierte Zugriffsmethoden für die Anweisungsquelle bereitzustellen Operanden. Ohne sie hätte jede Anweisung spezialisiert für jeden Typ, um auf den Quelloperanden mit dem korrekten As<int type>(). Die Definitionen dieser Vorlage Funktionen in instruction.h

Wie Sie sehen, gibt es drei Implementierungen, je nachdem, ob die Quelle Operandtypen sind mit dem Ziel identisch, unabhängig davon, ob das Ziel oder ob sie alle unterschiedlich sind. Jede Version von nimmt die Funktion einen Zeiger auf die Anweisungsinstanz sowie ein Callable (einschließlich Lambda-Funktionen). Das bedeutet, dass add jetzt neu geschrieben werden kann. semantische Funktion oben so:

void MyAddFunction(Instruction *inst) {
  generic::BinaryOp<uint32_t>(inst,
                              [](uint32_t a, uint32_t b) { return a + b; });
}

Bei Kompilierung mit bazel build -c opt und copts = ["-O3"] im Build sollte die Datei ohne Mehraufwand vollständig inline eingefügt werden. Kurzfassung und ohne Leistungseinbußen.

Wie bereits erwähnt, gibt es Hilfsfunktionen für unäre, binäre und ternäre Skalarfunktionen Anweisungen und Vektoräquivalente. Sie dienen auch als nützliche Vorlagen, .


Erster Build

Wenn Sie das Verzeichnis nicht in riscv_semantic_functions geändert haben, tun Sie dies jetzt. . Erstellen Sie dann das Projekt wie folgt – dieser Build sollte erfolgreich sein.

$  bazel build :riscv32i
...<snip>...

Es werden keine Dateien generiert, daher ist dies nur ein Probelauf, um sicherzustellen, dass alles in Ordnung ist.


Drei Operand-ALU-Anweisungen hinzufügen

Fügen wir nun die semantischen Funktionen für eine generische 3-Operanden-ALU hinzu. Anleitung. Öffnen Sie die Datei rv32i_instructions.cc und prüfen Sie, ob alle Fehlende Definitionen werden der Datei rv32i_instructions.h hinzugefügt.

Folgende Anweisungen werden hinzugefügt:

  • add: Addition von 32-Bit-Ganzzahlen.
  • and: 32-Bit-Bitweise und.
  • or: 32-Bit-Bitweise oder.
  • sll: logische 32-Bit-Verschiebung nach links.
  • sltu: 32-Bit-Zeichen ohne Vorzeichen ist kleiner als.
  • sra: 32-Bit-arithmetische Verschiebung nach rechts.
  • srl: logische 32-Bit-Verschiebung nach rechts.
  • sub: 32-Bit-Ganzzahlsubtraktion.
  • xor: 32-Bit-Bitweises XOR.

Wenn Sie die vorherigen Anleitungen durchgegangen sind, erinnern Sie sich vielleicht, dass wir zwischen zwischen Anweisungen für Registrierung und sofortiger Registrierung des Decoders. Für semantische Funktionen ist das nicht mehr erforderlich. Die Operanden-Schnittstellen lesen den Operandenwert aus dem Operanden ist, registrieren oder unmittelbar, wobei die semantische Funktion völlig unabhängig von was der zugrunde liegende Quelloperanden ist.

Mit Ausnahme von sra können alle oben genannten Anweisungen als Bedienelemente Vorzeichenlose 32-Bit-Werte, also können wir dafür die Vorlagenfunktion BinaryOp verwenden. die wir zuvor mit nur dem einzigen Vorlagentyp-Argument gesprochen haben. Füllen Sie die Felder Funktionskörper in rv32i_instructions.cc entsprechend. Beachten Sie, dass nur die niedrigen 5 Bits des zweiten Operanden für die Verschiebeanweisungen werden für die Verschiebung Betrag. Andernfalls haben alle Vorgänge die Form 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

Für sra verwenden wir die Vorlage BinaryOp mit drei Argumenten. Wenn Sie sich die Vorlage ist das erste Typargument der Ergebnistyp uint32_t. Das zweite ist den Typ des Quelloperanden 0, in diesem Fall int32_t, und der letzte ist der Typ des Quelloperanden 1, in diesem Fall uint32_t. Dadurch wird der Text von sra Semantische Funktion:

  generic::BinaryOp<uint32_t, int32_t, uint32_t>(
      instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });

Nehmen Sie die Änderungen vor und erstellen Sie den Build. Sie können Ihre Arbeit anhand rv32i_instructions.cc


Zwei Operand-ALU-Anweisungen hinzufügen

Es gibt nur zwei Anleitungen für 2-Operanden-ALU: lui und auipc. Erster Kopiert den vorverschobenen Quelloperanden direkt in das Ziel. Letzteres fügt die Anweisungsadresse zur unmittelbaren Angabe hinzu, bevor sie in den Ziel. Die Anweisungsadresse kann über die Methode address() aufgerufen werden des Instruction-Objekts.

Da es nur einen einzigen Quelloperanden gibt, kann BinaryOp nicht verwendet werden. müssen wir UnaryOp verwenden. Da wir sowohl die Quelle als auch den Zieloperanden als uint32_t können wir die Vorlage für ein einzelnes Argument verwenden. 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);
}

Der Text der semantischen Funktion für lui ist in etwa so einfach wie möglich. geben Sie einfach die Quelle zurück. Mit der semantischen Funktion für auipc wird eine kleinere da Sie auf die Methode address() in der Datei Instruction Instanz. Die Antwort besteht darin, der Lambda-Aufnahme instruction hinzuzufügen, zur Verwendung im Lambda-Funktionsrumpf. Anstelle von [](uint32_t a) { ... } wie zuvor sollte die Lambda-Funktion [instruction](uint32_t a) { ... } geschrieben werden. Jetzt kann instruction im Lambda-Text verwendet werden.

Nehmen Sie die Änderungen vor und erstellen Sie den Build. Sie können Ihre Arbeit anhand rv32i_instructions.cc


Anleitung zum Ändern des Kontrollflusses hinzufügen

Die zu implementierenden Änderungsanleitungen für den Kontrollfluss sind unterteilt. zu bedingten Zweiganweisungen (kürzere Zweige, die ausgeführt werden, wenn ein Vergleich wahr) und Jump-and-Link-Anweisungen, die verwendet werden, Funktionsaufrufe implementieren (und "-and-link" wird entfernt, indem der Link Register auf null setzen, sodass diese Schreibvorgänge No-Ops sind).

Bedingte Zweiganleitung hinzufügen

Es gibt keine Hilfsfunktion für Verzweigungen, daher gibt es zwei Optionen. Schreiben Sie die semantischen Funktionen von Grund auf oder eine lokale Hilfsfunktion. Da wir eine Anleitung mit 6 Zweigen implementieren müssen, scheint Letzteres die Hilfe zu lohnen. zu konzentrieren. Sehen wir uns vorher die Implementierung eines Zweigs an eine semantische Anweisung von Grund auf neu erstellen.

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();
  }
}

Das Einzige, was je nach Anleitung variiert, ist der Zweig und die Datentypen (vorzeichenbehaftet oder ohne Vorzeichen, 32-Bit-Ganzzahl) der beiden Quelloperanden. Das bedeutet, dass wir einen Vorlagenparameter für die Quelloperanden. Die Hilfsfunktion selbst muss die Instruction und ein aufrufbares Objekt wie std::function, das bool zurückgibt als Parameter. Die Hilfsfunktion würde so aussehen:

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();
  }
}

Jetzt können wir die semantische Funktion bge (vorzeichenbehafteter Zweig größer oder gleich) schreiben als:

void RV32IBge(Instruction *instruction) {
  BranchConditional<int32_t>(instruction,
                             [](int32_t a, int32_t b) { return a >= b; });
}

Im Folgenden sind die weiteren Anweisungen für den Zweig aufgeführt:

  • Beq – Zweig gleich.
  • Bgeu: Zweig größer oder gleich (ohne Vorzeichen).
  • BLt - Zweig kleiner als (signiert).
  • Bltu - Zweig kleiner als (unsigniert).
  • Bne - Zweig ungleich.

Nehmen Sie die Änderungen vor, um diese semantischen Funktionen zu implementieren, neu aufbauen. Sie können Ihre Arbeit anhand rv32i_instructions.cc

Es macht keinen Sinn, eine Hilfsfunktion für den Jump und Link zu schreiben. und müssen sie komplett neu schreiben. Beginnen wir mit die Semantik der Unterricht zu überprüfen.

Die Anweisung jal nimmt einen Versatz vom Quelloperanden 0 an und fügt ihn dem Aktuellen PC (Anweisungsadresse) zum Berechnen des Jump-Ziels. Das Sprungziel in den Zieloperanden 0 geschrieben. Die Rücksendeadresse ist die Adresse des nächsten sequenziellen Anweisung. Er kann berechnet werden, indem der aktuelle Wert in die Adresse einfügen. Die Rücksendeadresse wird an Zieloperand 1. Denken Sie daran, den Zeiger des Anweisungsobjekts der Lambda-Aufnahme.

Der jalr-Befehl verwendet ein Basisregister als Quelloperand 0 und einen Offset als Quelloperand 1 und addiert sie, um das Jump-Ziel zu berechnen. Ansonsten entspricht sie der jal-Anweisung.

Schreiben Sie basierend auf diesen Beschreibungen der Anweisungssemantik die beiden und Build. Sie können Ihre Arbeit anhand rv32i_instructions.cc


Anleitung für Speicher hinzufügen

Es gibt drei Speicheranweisungen, die wir implementieren müssen: Speicherbyte (sb), Halbwort speichern (sh) und Wort speichern (sw). Anleitung für Händler unterscheiden sich von der bisher implementierten Anleitung dadurch, in den lokalen Prozessorstatus schreiben. Stattdessen schreiben sie in eine Systemressource: Hauptspeicher. MPACT-Sim behandelt den Speicher nicht als Befehlsoperanden, Der Speicherzugriff muss daher mit einer anderen Methode durchgeführt werden.

Die Antwort besteht darin, dem ArchState-Objekt MPACT-Sim Speicherzugriffsmethoden hinzuzufügen. Erstellen Sie ein neues RiscV-Statusobjekt, das von ArchState abgeleitet ist. in der dies hinzugefügt werden kann. Das ArchState-Objekt verwaltet Kernressourcen wie Registern und anderen Zustandsobjekten. Es verwaltet auch die Verspätungslinien, die Zieloperandendaten zwischenspeichern, bis sie wieder geschrieben werden können. die Register-Objekte. Die meisten Anweisungen können ohne Wissen über aber einige, wie z. B. Speichervorgänge und andere spezifische Systeme Anweisungen erfordern eine Funktionalität, die sich in diesem Zustandsobjekt befindet.

Sehen wir uns die semantische Funktion für die fence-Anweisung an, bereits in rv32i_instructions.cc als Beispiel implementiert. Das fence Anweisung hält Anweisungsproblem zurück, bis bestimmte Speichervorgänge abgeschlossen. Er wird verwendet, um die Speicherreihenfolge zwischen Anweisungen zu gewährleisten vor der Anweisung und denen, die danach ausgeführt werden.

// 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);
}

Der wichtigste Teil der semantischen Funktion der fence-Anweisung sind die letzten beiden Linien. Zuerst wird das Zustandsobjekt mit einer Methode im Instruction abgerufen. und downcast<> auf die RiscV-spezifische abgeleitete Klasse. Dann der Fence der Klasse RiscVState wird aufgerufen, um den Fencing-Vorgang auszuführen.

Anweisungen für Händler funktionieren ähnlich. Erstens die effektive Adresse des Der Speicherzugriff wird aus den Quelloperanden des Basis- und Offset-Befehls berechnet. wird der zu speichernde Wert vom nächsten Quelloperanden abgerufen. Als Nächstes Das RiscV-Statusobjekt wird über den state()-Methodenaufruf abgerufen. static_cast<>, und die entsprechende Methode wird aufgerufen.

Die Methode StoreMemory des RiscVState-Objekts ist relativ einfach, hat aber einen Folgen, die wir kennen müssen:

  void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);

Wie Sie sehen, benötigt die Methode drei Parameter: Anweisung selbst, die Adresse des Geschäfts und einen Verweis auf einen DataBuffer Instanz, die die Geschäftsdaten enthält. Beachten Sie, dass keine Größe erforderlich ist, der DataBuffer-Instanz selbst enthält eine size()-Methode. Es gibt jedoch keine Zieloperanden, auf den die Anweisung zugreifen kann, die für Ordnen Sie eine DataBuffer-Instanz der entsprechenden Größe zu. Stattdessen müssen wir DataBuffer-Factory aus der Methode db_factory() in die Instanz Instruction. Die Factory hat die Methode Allocate(int size) das eine DataBuffer-Instanz der erforderlichen Größe zurückgibt. Hier ist ein Beispiel wie Sie damit eine DataBuffer-Instanz für einen Halb-Wortspeicher zuweisen können (Hinweis: auto ist eine C++-Funktion, die den Typ von der rechten Hand ableitet. Seite der Aufgabe):

  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(uint16_t));

Sobald wir die DataBuffer-Instanz haben, können wir wie gewohnt in sie schreiben:

  db->Set<uint16_t>(0, value);

Übergeben Sie ihn dann an die Speicherspeicher-Schnittstelle:

  state->StoreMemory(instruction, address, db);

Wir sind noch nicht ganz fertig. Die Instanz DataBuffer wird als Referenz gezählt. Dieses normalerweise von der Submit-Methode verstanden und verarbeitet werden, damit die Anwendungsfall so einfach wie möglich zu machen. StoreMemory ist jedoch nicht so geschrieben. Es IncRef die DataBuffer Instanz während des Betriebs und dann DecRef. Wenn die semantische Funktion jedoch DecRef als eigene Referenz verwendet, wird sie niemals zurückgefordert. Daher enthält die letzte Zeile lautet:

  db->DecRef();

Es gibt drei Speicherfunktionen. Der einzige Unterschied, der sich unterscheidet, ist die Größe der den Speicherzugriff. Das klingt nach einer großen Chance für einen anderen Ortsansässigen, Vorlagen-Hilfsfunktion. Der einzige Unterschied bei der Funktion Der Typ des Speicherwerts, daher muss die Vorlage diesen als Argument enthalten. Ansonsten muss nur die Instanz Instruction übergeben werden:

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();
}

Fahren Sie fort, schließen Sie die semantischen Funktionen ab und erstellen Sie den Build. Sie können Ihre Gegen rv32i_instructions.cc


Anleitung für die Arbeitsspeicherauslastung hinzufügen

Die folgenden Ladeanweisungen müssen implementiert werden:

  • lb: Lädt Byte, signierenerweitern in ein Wort.
  • lbu: Lädt Byte ohne Vorzeichen, nullerweitert in ein Wort.
  • lh: Ein halbes Wort wird geladen, durch Vorzeichen erweitert in ein Wort.
  • lhu: Ein halbes Wort ohne Vorzeichen wird mit Null-Erweiterung in ein Wort geladen.
  • lw – Wort laden.

Die Ladeanweisungen sind die komplexesten Anweisungen zum Modellieren. dieser Anleitung. Sie ähneln den Anweisungen für das Geschäft: Sie greifen auf das RiscVState-Objekt zu, erhöhen jedoch die Komplexität, da jeder Ladevorgang in zwei separate semantische Funktionen unterteilt. Die erste ist ähnlich wie die Filialanweisung, da sie die effektive Adresse und initiiert den Speicherzugriff. Die zweite wird ausgeführt, wenn der Speicher und schreibt die Speicherdaten in das Register Operand in das Feld.

Sehen wir uns zuerst die Deklaration der Methode LoadMemory in RiscVState an:

  void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
                  Instruction *child_inst, ReferenceCount *context);

Im Vergleich zur StoreMemory-Methode benötigt LoadMemory zwei zusätzliche Parameter: ein Zeiger auf eine Instruction-Instanz und ein Zeiger auf eine Referenz gezähltes context-Objekt Die erste ist die Anweisung child, Implementiert das Register Write-Back (wie in der ISA-Decoder-Anleitung beschrieben). Es Der Zugriff erfolgt über die Methode child() in der aktuellen Instruction-Instanz. Letzteres ist ein Zeiger auf eine Instanz einer Klasse, die von ReferenceCount, die in diesem Fall eine DataBuffer-Instanz speichert, die die geladenen Daten enthalten. Das Kontextobjekt ist über die Methode context() im Instruction-Objekt (für die meisten Anweisungen ist auf nullptr festgelegt).

Das Kontextobjekt für RiscV-Speicherlasten ist wie folgt definiert:

// 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;
};

Die Ladeanweisungen sind alle gleich, mit Ausnahme der Datengröße (Byte, Halbwort und Wort) und ob der geladene Wert vorzeichenerweitert ist oder nicht. Die Letztere berücksichtigt nur die child-Anweisung. Erstellen wir eine Vorlage Hilfsfunktion für die Anweisungen zum Hauptladen angezeigt. Sie ist dem Beispiel die Anweisung speichern, außer auf einen Quelloperanden, um einen Wert abzurufen, und erstellt ein Kontextobjekt.

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();
}

Wie Sie sehen, besteht der Hauptunterschied darin, dass die zugewiesene DataBuffer-Instanz wird sowohl als Parameter an den LoadMemory-Aufruf übergeben als auch im LoadContext-Objekt.

Die semantischen Funktionen der Anweisung child sind alle sehr ähnlich. Zunächst sollte der LoadContext wird durch Aufrufen der Instruction-Methode context() abgerufen. statisch in die LoadContext * umgewandelt. Zweitens wird der Wert (laut den Daten Typ) wird aus der Load-Data-Instanz DataBuffer gelesen. Drittens: Eine neue Die Instanz DataBuffer wird vom Zieloperanden zugewiesen. Die Funktion geladener Wert wird in die neue DataBuffer-Instanz geschrieben und Submit übergeben. Auch hier ist eine vorlagenbasierte Hilfsfunktion eine gute Idee:

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();
}

Implementieren Sie nun die letzten Hilfsfunktionen und semantischen Funktionen. Bezahlen Achten Sie auf den Datentyp, den Sie in der Vorlage für jede Hilfsfunktion verwenden. und dass er der Größe und der Art der Last mit Vorzeichen und ohne Vorzeichen entspricht, Anleitung.

Sie können Ihre Arbeit anhand rv32i_instructions.cc


Endgültigen Simulator erstellen und ausführen

Jetzt können wir den endgültigen Simulator erstellen. Die Top-Level-C++-Bibliotheken, die alle Arbeiten in diesen Tutorials zusammenfassen, in other/. Sie müssen sich diesen Code nicht zu intensiv ansehen. Mi. dieses Thema in einer späteren Anleitung für Fortgeschrittene behandelt.

Ändern Sie Ihr Arbeitsverzeichnis in other/ und erstellen Sie den Build. Es sollte ohne Fehler.

$ cd ../other
$ bazel build :rv32i_sim

In diesem Verzeichnis befindet sich ein einfaches „Hello World“- Programm in der Datei hello_rv32i.elf So führen Sie den Simulator für diese Datei aus und sehen sich die Ergebnisse an:

$ bazel run :rv32i_sim -- other/hello_rv32i.elf

Sie sollten etwas Ähnliches sehen:

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
$

Der Simulator kann auch mit dem Befehl bazel run :rv32i_sim -- -i other/hello_rv32i.elf im interaktiven Modus ausgeführt werden. Daraufhin wird eine einfache Command-Shell. Geben Sie help in die Eingabeaufforderung ein, um die verfügbaren Befehle anzuzeigen.

$ 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] >

Damit ist diese Anleitung abgeschlossen. Wir hoffen, dass Ihnen diese Informationen weiterhelfen.