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 ior
– 32-bitowa (bitowa) lubsll
– 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.
Dodawanie instrukcji dodawania linków
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.