Samouczek dotyczący funkcji semantycznych w instrukcjach

Cele tego samouczka to:

  • Dowiedz się, jak funkcje semantyczne są używane do implementacji semantyki instrukcji.
  • Dowiedz się, jak funkcje semantyczne są powiązane z opisem dekodera ISA.
  • Napisz funkcje semantyczne instrukcji dla instrukcji RiscV RV32I.
  • Przetestuj końcowy symulator, uruchamiając mały komunikat „Hello World” .

Omówienie funkcji semantycznych

Funkcja semantyczna w MPACT-Sim to funkcja, która implementuje operację instrukcji, aby jej efekty uboczne były widoczne w stanie symulowanym. w taki sam sposób, w jaki efekty uboczne instrukcji są widoczne podczas jej wykonania sprzęt. Wewnętrzna reprezentacja każdej zdekodowanej instrukcji przez symulator zawiera obiekt możliwy do wywołania, który jest używany do wywoływania dla niego funkcji semantycznej wraz z instrukcjami.

Funkcja semantyczna ma podpis void(Instruction *), czyli która zwraca wskaźnik do instancji klasy Instruction, zwraca void.

Klasa Instruction jest zdefiniowana w: instruction.h

Do pisania funkcji semantycznych szczególnie interesują nas wektory interfejsu źródłowego i docelowego operandu, do których dostęp uzyskuje się za pomocą Wywołania metod Source(int i) i Destination(int i).

Poniżej przedstawiono interfejsy operandu źródłowego i docelowego:

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

Podstawowy sposób zapisu funkcji semantycznej dla normalnego 3-operandu taka jak 32-bitowa instrukcja add wygląda tak:

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

Przeanalizujmy elementy tej funkcji. Dwa pierwsze wiersze kolumny treść funkcji odczytuje z operandów źródłowych 0 i 1. Rozmowa AsUint32(0) interpretuje dane bazowe jako tablica uint32_t i pobiera wartość 0. . Dzieje się tak niezależnie od tego, czy bazowy rejestr lub wartość to lub bez wartości. Rozmiar operandu źródłowego (w elementach) może być uzyskane z metody źródłowej shape(), która zwraca wektor który zawiera liczbę elementów w każdym wymiarze. Ta metoda zwraca {1} dla skalarnego, {16} dla wektora 16-elementowego i {4, 4} dla tablicy 4 x 4.

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

Następnie do elementu tymczasowego uint32_t o nazwie c zostaje przypisana wartość a + b.

Następny wiersz może wymagać więcej wyjaśnień:

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

Obiekt DataBuffer to obiekt zliczany jako odwołanie, który służy do przechowywania wartości symulowany stan, np. rejestry. Jest dość niewpisany, chociaż na podstawie obiektu, z którego został przydzielony. W tym przypadku jest to rozmiar sizeof(uint32_t) Ta instrukcja przydziela nowy bufor danych o rozmiarze miejsce docelowe, które jest celem tego operandu miejsca docelowego – w tym przypadku 32-bitowy rejestr całkowity. Obiekt DataBuffer jest również inicjowany przez klucz architektoniczny czas oczekiwania dla instrukcji. To określa się podczas instrukcji do ich dekodowania.

Następny wiersz traktuje instancję bufora danych jako tablicę obiektów uint32_t oraz zapisuje wartość zapisaną w c do 0 elementu.

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

Ostatnie stwierdzenie przesyła bufor danych do symulatora, który ma zostać użyty jako nową wartość docelowego stanu maszyny (w tym przypadku rejestru) po czas oczekiwania instrukcji ustawiony przy jej zdekodowaniu oraz wektor docelowy operandu został wypełniony.

To dość krótka funkcja, ale jest w niej powtarzalny. który staje się powtarzalny przy wdrażaniu instrukcji po instrukcji. Dodatkowo może to zaburzyć semantykę instrukcji. W zamówieniu aby jeszcze bardziej uprościć pisanie funkcji semantycznych dla większości instrukcji, jest wiele funkcji pomocniczych zdefiniowanych na podstawie szablonu instruction_helpers.h. Ci pomocnicy ukrywają stały kod w przypadku instrukcji dotyczących 1, 2 lub 3 operandy źródłowe i pojedynczy operand miejsca docelowego. Przyjrzyjmy się dwóm funkcje pomocnicze operandu:

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

Zamiast używać takich oświadczeń jak:

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

Funkcja pomocnicza używa:

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

GetInstructionSource to rodzina funkcji pomocniczych opartych na szablonach, służą do zapewniania szablonowych metod dostępu do źródła instrukcji operandy. Bez nich każda funkcja pomocnicza instrukcji do specyfiki każdego typu, aby uzyskać dostęp do operandu źródłowego z poprawnym As<int type>(). Definicje tych szablonów możesz zobaczyć funkcje w instruction.h.

Jak widać, są 3 implementacje w zależności od tego, czy źródło typy operandów są takie same jak miejsce docelowe, niezależnie od tego, czy jest to miejsce docelowe różnią się od źródeł, czy też są różne. Każda wersja funkcja wykorzystuje wskaźnik do instancji instrukcji oraz obiekt umożliwiający wywołanie (obejmuje to funkcje lambda). Oznacza to, że możemy teraz zmodyfikować add funkcję semantyczną w ten sposób:

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

Po skompilowaniu z elementami bazel build -c opt i copts = ["-O3"] w kompilacji powinien być w pełni zintegrowany, bez narzutu, co da nam wkład zwięzłość bez kar za wyniki.

Jak już wspomnieliśmy, dostępne są funkcje pomocnicze dla skalarnego jednoargumentowego, binarnego i trójwartościowego. jak i wektorowe odpowiedniki. Stanowią też przydatne szablony do tworzenia własnych instrukcji, które nie pasują do ogólnego formatu.


Pierwsza kompilacja

Jeśli katalog nie został zmieniony na riscv_semantic_functions, zrób to teraz. Potem utwórz projekt w podany niżej sposób – kompilacja powinna się udać.

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

Żadne pliki nie są generowane, więc to tylko uruchomienie próbne, sprawdź, czy wszystko jest w porządku.


Dodaj 3 instrukcje dotyczące ALU argumentów

Teraz dodajmy funkcje semantyczne dla ogólnego, 3-operandowego ALU. za instrukcje. Otwórz plik rv32i_instructions.cc i upewnij się, że wszystkie brakujące definicje są dodawane do pliku rv32i_instructions.h w miarę upływu czasu.

Instrukcje, które dodamy:

  • add – dodawanie 32-bitowej liczby całkowitej.
  • and – 32-bitowy i
  • or – 32-bitowa (bitowa) lub
  • sll – 32-bitowe przesunięcie logiczne w lewo.
  • sltu – 32-bitowy niepodpisany zestaw o wartości mniejszej niż.
  • sra – 32-bitowe przesunięcie arytmetyczne w prawo.
  • srl – 32-bitowe przesunięcie logiczne w prawo.
  • sub – odejmowanie 32-bitowej liczby całkowitej.
  • xor – 32-bitowy xor.

Jeśli znasz już nasze poprzednie samouczki, pewnie pamiętasz, że wyróżniamy między instrukcjami dotyczącymi rejestracji a natychmiastowymi instrukcjami rejestracji w za pomocą dekodera. W przypadku funkcji semantycznych nie musimy już tego robić. Interfejsy operandu będą odczytywać wartość operandu z dowolnego operandu jest rejestrowana lub natychmiastowa, przy czym funkcja semantyczna jest całkowicie niezależna od funkcji jaki jest właściwy operand źródłowy.

Z wyjątkiem sra wszystkie powyższe instrukcje mogą być traktowane jako operacje na 32-bitowe niepodpisane wartości, więc w ich przypadku możemy użyć funkcji szablonu BinaryOp wspomnieliśmy wcześniej tylko z jednym argumentem typu szablonu. Wypełnij odpowiednio treści funkcji w zasadzie rv32i_instructions.cc. Pamiętaj, że tylko niskie 5 części drugiego operandu do instrukcji przesunięcia są używane do tego przesunięcia kwotę. W przeciwnym razie wszystkie operacje mają postać 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

W przypadku sra użyjemy szablonu BinaryOp z 3 argumentami. Patrząc na szablonem, pierwszym argumentem typu jest typ wyniku uint32_t. Druga to typ operandu źródłowego 0, w tym przypadku int32_t, a ostatni to typ operandu źródłowego 1, w tym przypadku uint32_t. W ten sposób sra funkcja semantyczna:

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

Wprowadź zmiany i zacznij budować. Możesz porównać swoją pracę z tymi, rv32i_instructions.cc.


Dodaj 2 instrukcje dotyczące ALU argumentów

Są tylko 2 instrukcje ALU z 2 operandami: lui i auipc. Poprzedni kopiuje wstępnie przesunięty operand źródłowy bezpośrednio do miejsca docelowego. To ostatnie dodaje adres instrukcji do ciągu bezpośredniego przed jego zapisaniem w miejsce docelowe. Adres instrukcji jest dostępny z poziomu metody address() obiektu Instruction.

Ponieważ istnieje tylko jeden operand źródłowy, nie możemy użyć zamiast tego funkcji BinaryOp. musimy użyć UnaryOp. Możemy analizować zarówno źródło, jak i operandy miejsca docelowego jako uint32_t, możemy użyć szablonu z jednym argumentem wersji.

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

Treść funkcji semantycznej lui jest maksymalnie prosta, po prostu zwraca źródło. Funkcja semantyczna funkcji auipc wprowadza liczbę osób niepełnoletnich bo musisz mieć dostęp do metody address() w Instruction instancji. Odpowiedź polega na dodaniu do przechwytywania lambda pola instruction, dzięki czemu dostępnych do użycia w treści funkcji lambda. Zamiast [](uint32_t a) { ... } (jak poprzednio) funkcja lambda powinna mieć zapis [instruction](uint32_t a) { ... }. Funkcja instruction może być teraz używana w treści lambda.

Wprowadź zmiany i zacznij budować. Możesz porównać swoją pracę z tymi, rv32i_instructions.cc.


Dodaj instrukcje zmiany procesu sterowania

Instrukcje dotyczące zmiany procesu sterowania, które musisz wdrożyć, są podzielone w instrukcje gałęzi warunkowych (krótsze gałęzie, które są wykonywane, porównanie jest prawdziwe) oraz instrukcje „przewijania i linku”, które służą do wdrożyć wywołania funkcji (link -and-link jest usuwany przez ustawienie linku do zera, co oznacza brak operacji).

Dodaj instrukcje gałęzi warunkowej

Nie ma funkcji pomocniczej dla instrukcji gałęzi, więc są 2 opcje. Wpisz funkcje semantyczne od zera lub utwórz lokalną funkcję pomocniczą. Musimy wdrożyć 6 instrukcji dotyczących gałęzi, więc ta druga opcja wydaje się warta wysiłek. Zanim to zrobimy, przyjrzymy się implementacji gałęzi funkcji semantycznych instrukcji.

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

Jedyne, co różni się w instrukcjach dotyczących gałęzi, to oraz typy danych (podpisane i niepodpisane 32-bitowe int). operandów źródłowych. Musimy mieć parametr szablonu dla parametru operandów źródłowych. Sama funkcja pomocnicza musi przejąć funkcję Instruction instancję i wywoływany obiekt, np. std::function, który zwraca bool . Funkcja pomocnicza będzie więc wyglądać tak:

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

Teraz możemy zapisać funkcję semantyczną bge (zewnętrzną gałąź ze znakiem większą lub równą) jako:

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

Pozostałe instrukcje dotyczące gałęzi są następujące:

  • Beq – gałąź równa się.
  • bgeu – gałąź większa lub równa (bez znaku).
  • Blt – gałąź mniejsza niż (znak)
  • Bltu – gałąź mniejsza niż (bez znaku).
  • Bne – gałąź nie jest równa.

Wprowadź zmiany, aby wdrożyć te funkcje semantyczne. na nowo. Możesz porównać swoją pracę z tymi, rv32i_instructions.cc.

Nie ma sensu pisać funkcji pomocniczej dla skoku i linku instrukcji, więc musimy napisać je od zera. Zacznijmy od Patrząc na semantykę instrukcji.

Instrukcja jal pobiera przesunięcie od operandu źródłowego 0 i dodaje je do bieżącego komputera (adres instrukcji) do obliczenia miejsca docelowego skoku. Cel skoku jest zapisywana w operandie miejsca docelowego 0. Adres zwrotny to adres w kolejnych instrukcjach sekwencyjnych. Aby ją obliczyć, dodaj bieżącą rozmiar instrukcji do jej adresu. Adres zwrotny jest wysyłany na adres 1 operand miejsca docelowego. Pamiętaj, aby umieścić wskaźnik obiektu instrukcji w obrazu lambda.

Instrukcja jalr pobiera rejestr podstawowy jako argument źródłowy 0 i przesunięcie jako operand 1 źródła i dodaje je razem, aby obliczyć miejsce docelowe przeskoku. W przeciwnym razie jest taka sama jak instrukcja jal.

Na podstawie tych opisów semantyki instrukcji opisz dwie semantykę: funkcji i kompilacji. Możesz porównać swoją pracę z tymi, rv32i_instructions.cc.


Instrukcje dodawania magazynu pamięci

Musimy wdrożyć trzy instrukcje dotyczące sklepu: store byte (sb), zapisz słowo półwyrazowe (sh) i słowo z sklepu (sw). Instrukcje dla sklepu różnią się od instrukcji zastosowanych do tej pory tym, że nie są do zapisu w stanie lokalnego procesora. Zamiast tego zapisują dane w zasobie systemowym, pamięci głównej. MPACT-Sim nie traktuje pamięci jako operandu instrukcji, więc dostęp do pamięci trzeba przeprowadzić przy użyciu innej metodologii.

Należy dodać metody dostępu do pamięci do obiektu MPACT-Sim ArchState, lub lepiej utwórz nowy obiekt stanu RiscV, który pochodzi z metody ArchState. gdzie to można dodać. Obiekt ArchState zarządza podstawowymi zasobami, takimi jak rejestry i inne obiekty stanu. Zarządza również liniami opóźnienia używanymi do buforują bufory danych operandu docelowego, aż będzie można je zapisać z powrotem w obiektów rejestru. Większość instrukcji można wdrożyć bez wiedzy tej klasy, ale niektóre, takie jak operacje pamięci i inne konkretne systemy instrukcje wymagają funkcji przechowywania w tym obiekcie stanu.

Przyjrzyjmy się funkcji semantycznej instrukcji fence, która już zaimplementowane w rv32i_instructions.cc. fence wstrzymuje problem z instrukcją do czasu, aż niektóre operacje w pamięci . Służy do gwarancji kolejności w pamięci między instrukcjami wykonywanych przed instrukcją i tych, które występują po niej.

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

Kluczową częścią funkcji semantycznej instrukcji fence są 2 ostatnie elementy . Najpierw obiekt stanu jest pobierany za pomocą metody w Instruction i downcast<> do klasy derywowanej RiscV. Potem Fence jest wywoływana metoda klasy RiscVState do wykonywania operacji ogrodzenia.

Instrukcje dotyczące sklepu będą działać podobnie. Najpierw ostateczny adres URL dostęp do pamięci jest obliczany na podstawie operandów źródła instrukcji bazowej i przesunięcia, wtedy wartość do zapisania jest pobierana z następnego operandu źródłowego. Następnie Obiekt stanu RiscV jest pobierany za pomocą wywołania metody state() oraz static_cast<> i wywoływana jest odpowiednia metoda.

Metoda StoreMemory obiektu RiscVState jest stosunkowo prosta, ale ma konsekwencji, o których musimy wiedzieć:

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

Jak widać, metoda ta przyjmuje 3 parametry: wskaźnik do sklepu, samej instrukcji, adresu sklepu oraz wskaźnika DataBuffer instancji, która zawiera dane sklepu. Uwaga: nie jest wymagany żaden rozmiar, Sama instancja DataBuffer zawiera metodę size(). Nie istnieje jednak żaden operand miejsca docelowego dostępny dla instrukcji, której można użyć do przydzielić instancję DataBuffer o odpowiednim rozmiarze. Zamiast tego musimy używa fabryki DataBuffer uzyskanej z metody db_factory() w instancję Instruction. Fabryka ma metodę Allocate(int size) , który zwraca instancję DataBuffer o wymaganym rozmiarze. Oto przykład: jak użyć tego do przydzielenia instancji DataBuffer do magazynu składającego się z półwyrazu (zwróć uwagę, że auto to funkcja w języku C++, która rozpoznaje typ z prawej ręki. stronie projektu):

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

Gdy mamy już instancję DataBuffer, możemy w niej zapisać jak zwykle:

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

Następnie przekaż go do interfejsu magazynu pamięci:

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

To jeszcze nie koniec. Wystąpienie DataBuffer jest liczone jako odwołanie. Ten jest zazwyczaj rozumieny i obsługiwany przez metodę Submit, więc aby w przypadku najczęstszych zastosowań, jak najprościej. StoreMemory nie jest jednak w ten sposób. Zostanie IncRef uruchomiona instancja DataBuffer na nim, a po zakończeniu DecRef. Jeśli jednak funkcja semantyczna nie DecRef ma własny plik referencyjny, więc nigdy nie zostanie odzyskany. Dlatego w ostatnim wierszu być:

  db->DecRef();

Istnieją trzy funkcje sklepu i różnią się tylko rozmiarem dostęp do pamięci. To doskonała okazja dla firmy działającej lokalnie jako szablonowej funkcji pomocniczej. Jedyną różnicą między funkcją sklepu jest to, typ wartości w sklepie, więc szablon musi zawierać ten argument jako argument. Poza tym w obiekcie musi zostać przekazane tylko wystąpienie Instruction:

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

Dokończ tworzenie funkcji semantycznych sklepu. Swoje działaj przeciwko rv32i_instructions.cc.


Dodawanie instrukcji dotyczących wczytywania pamięci

Instrukcje wczytywania, które należy zaimplementować:

  • lb – wczytaj bajt, rozszerz znak do słowa.
  • lbu – wczytaj bajt bez znaku i rozciągnij do słowa.
  • lh – wczytaj półwyrazu, rozszerz znak na słowo.
  • lhu – wczytuj półwyrazu bez znaku i rozszerzając je do zera.
  • lw – wczytaj słowo.

Instrukcje wczytywania to najbardziej złożone instrukcje, które musimy modelować w tym samouczku. Działają podobnie do instrukcji sklepu, ponieważ wymagają dostęp do obiektu RiscVState, ale komplikuje to, że każde wczytywanie są podzielone na dwie różne funkcje semantyczne. Pierwsza to podobnie jak w przypadku instrukcji sklepu, ponieważ oblicza on efektywny adres i uruchamia dostęp do pamięci. Druga jest wykonywana, gdy pamięć dostęp jest ukończony i zapisuje dane pamięci w miejscu docelowym rejestru .

Zacznijmy od omówienia deklaracji metody LoadMemory w RiscVState:

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

W porównaniu z metodą StoreMemory funkcja LoadMemory wymaga 2 dodatkowych parametry: wskaźnik do instancji Instruction i wskaźnik odwołanie liczone jako context obiekt. Pierwszy z nich to instrukcja podrzędna, implementuje zapis zwrotny rejestru (opisany w samouczku na temat dekodera ISA). it jest uzyskiwany dostęp przy użyciu metody child() w bieżącej instancji Instruction. Ten ostatni parametr jest wskaźnikiem do wystąpienia klasy wywodzącej się z ReferenceCount, który w tym przypadku przechowuje instancję DataBuffer, która zostanie zawierają wczytane dane. Obiekt kontekstu jest dostępny przez context() w obiekcie Instruction (chociaż w przypadku większości instrukcji (wartość: nullptr).

Obiekt kontekstu wczytywania pamięci RiscV jest zdefiniowany jako ta struktura:

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

Instrukcje ładowania są takie same, z wyjątkiem rozmiaru danych (bajtów, połowa słowa i słowa) oraz czy wczytana wartość jest rozszerzona. ten ostatni uwzględnia jedynie instrukcję dzieci. Utwórzmy szablon dla instrukcji wczytywania głównego. Będzie ona bardzo podobna do instrukcji w magazynie, z wyjątkiem tego, że nie uzyska ona dostępu do operandu źródłowego w celu pobrania wartości, i tworzy obiekt kontekstu.

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

Jak widać, główną różnicą jest to, że przydzielona instancja DataBuffer jest przekazywany do wywołania LoadMemory jako parametr oraz przechowywany w funkcji LoadContext obiekt.

Funkcje semantyczne instrukcji podrzędnych są bardzo podobne. Po pierwsze, Wartość LoadContext jest uzyskiwana przez wywołanie metody Instruction context() oraz jest statyczny na LoadContext *. Po drugie, wartość (według danych type) jest odczytywany z instancji DataBuffer z danymi load. Po trzecie, Instancja DataBuffer jest przydzielana z operandu miejsca docelowego. Pamiętaj też, że wczytana wartość jest zapisywana w nowej instancji DataBuffer, a Submit. Przypominam, że warto użyć funkcji pomocniczej opartej na szablonie:

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

Możesz teraz wdrożyć te ostatnie funkcje pomocnicze i funkcje semantyczne. Zapłać zwróć uwagę na typ danych używany w szablonie dla każdej funkcji pomocniczej. i odpowiadała rozmiarowi oraz podpisanemu/niepodpisanemu charakterowi obciążenia. wraz z instrukcjami.

Możesz porównać swoją pracę z tymi, rv32i_instructions.cc.


Tworzenie i uruchamianie ostatecznego symulatora

Mamy już za sobą wszystkie trudne słowa, więc możemy stworzyć ostateczny symulator. najwyższego poziomu bibliotek C++, które łączą całą pracę z tych samouczków, w lokalizacji: other/. Nie musisz za bardzo przyglądać się temu kodowi. Śr Zajrzyj do tego tematu w przyszłej wersji samouczka dla zaawansowanych.

Zmień katalog roboczy na other/ i skompiluj. Powinien tworzyć się bez .

$ cd ../other
$ bazel build :rv32i_sim

Znajdziesz w nim prosty „hello world” program w pliku hello_rv32i.elf Aby uruchomić symulator dla tego pliku i wyświetlić wyniki:

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

Powinno wyświetlić się coś takiego:

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
$

Symulator można również uruchomić w trybie interaktywnym za pomocą polecenia bazel run :rv32i_sim -- -i other/hello_rv32i.elf. Pozwala to uzyskać prostą powłoki poleceń. Aby zobaczyć dostępne polecenia, wpisz w nim help.

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

To koniec samouczka. Mamy nadzieję, że te informacje okażą się pomocne.