Cele tego samouczka to:
- Dowiedz się, jak wygenerowane dekodery ISA i dekodery binarne współpracują ze sobą.
- Napisz niezbędny kod w C++, aby utworzyć pełny dekoder instrukcji dla RiscV. RV32I, który łączy dekodery ISA i dekodery binarne.
Korzystanie z dekodera instrukcji
Dekoder instrukcji odpowiada za odczytanie, pod kątem adresu instrukcji,
słowa instrukcji z pamięci i zwraca w pełni zainicjowaną instancję
Instruction
, który reprezentuje tę instrukcję.
Dekoder najwyższego poziomu implementuje poniższy kod generic::DecoderInterface
:
// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
public:
// Return a decoded instruction for the given address. If there are errors
// in the instruciton decoding, the decoder should still produce an
// instruction that can be executed, but its semantic action function should
// set an error condition in the simulation when executed.
virtual Instruction *DecodeInstruction(uint64_t address) = 0;
virtual ~DecoderInterface() = default;
};
Jak widać, należy wdrożyć tylko jedną metodę: cpp
virtual Instruction *DecodeInstruction(uint64_t address);
Przyjrzyjmy się teraz, co zawiera wygenerowany kod, a co jest potrzebne.
Najpierw zwróć uwagę na klasę najwyższego poziomu RiscV32IInstructionSet
w pliku
riscv32i_decoder.h
, który został wygenerowany na koniec samouczka w dniu
dekodera ISA. Aby ponownie zobaczyć zawartość, przejdź do katalogu rozwiązań
i stworzyć wszystko od nowa.
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
Zmień katalog z powrotem na katalog główny repozytorium i sprawdźmy,
w wygenerowanych źródłach. Aby to zrobić, zmień katalog na
bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(zakładając, że używasz procesora x86
host – w przypadku innych hostów kolejnym ciągiem znaków będzie k8-fastbuild).
$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
Zobaczysz listę czterech plików źródłowych zawierających wygenerowany kod w C++:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Otwórz pierwszy plik riscv32i_decoder.h
. Istnieją 3 zajęcia,
na które trzeba zwrócić uwagę:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
Zwróć uwagę na nazwy klas. Nazwa wszystkich zajęć zależy od
Wersja nazwy podana w polu „isa” (z uwzględnieniem alfabetu łacińskiego) w tym pliku:
isa RiscV32I { ... }
Zacznijmy od zajęć RiscVIInstructionSet
. Widać ją poniżej:
class RiscV32IInstructionSet {
public:
RiscV32IInstructionSet(ArchState *arch_state,
RiscV32IInstructionSetFactory *factory);
Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);
private:
std::unique_ptr<Riscv32Slot> riscv32_decoder_;
ArchState *arch_state_;
};
Na tych zajęciach nie ma metod wirtualnych, jest to więc klasa samodzielna, ale
zauważyć 2 rzeczy. Najpierw konstruktor wskazuje na instancję
RiscV32IInstructionSetFactory
zajęcia. Jest to klasa wygenerowana przez
za pomocą dekodera do utworzenia instancji klasy RiscV32Slot
, która służy do
zdekodować wszystkie instrukcje zdefiniowane dla obiektu slot RiscV32
, jak określono w
riscv32i.isa
. Po drugie, metoda Decode
pobiera dodatkowy parametr
wskaźnika typu do RiscV32IEncodingBase
, to klasa, która zwróci
interfejsu między dekoderem isa wygenerowanym w pierwszym samouczku a plikiem binarnym
za pomocą dekodera
wygenerowanego w drugim module.
Klasa RiscV32IInstructionSetFactory
jest klasą abstrakcyjną, z której
własnego dekodera. W większości przypadków
klasa jest prosta: podaj metodę wywoływania konstruktora dla każdego
klasę przedziałów zdefiniowaną w pliku .isa
. W naszym przypadku jest to bardzo proste,
jest tylko jedną taką klasą: Riscv32Slot
(pascal-case w nazwie riscv32
połączony z Slot
). Metoda nie jest generowana, ponieważ
w zaawansowanych przypadkach użycia, w których może być przydatne do pobierania podklasy.
z boksu i wywoływać jego konstruktor.
W dalszej części tego szkolenia omówimy ostatnie zajęcia RiscV32IEncodingBase
.
bo to jest temat innego ćwiczenia.
Zdefiniuj dekoder instrukcji najwyższego poziomu
Zdefiniuj klasę fabryki
Jeśli projekt został ponownie skompilowany w ramach pierwszego samouczka, pamiętaj, aby przejść z powrotem na
w katalogu riscv_full_decoder
.
Otwórz plik riscv32_decoder.h
. Wszystkie niezbędne pliki „Uwzględnij” zostały
już dodano, a przestrzenie nazw zostały skonfigurowane.
Zdefiniuj zajęcia po komentarzu oznaczonym jako //Exercise 1 - step 1
.
Pole RiscV32IsaFactory
odziedziczone z grupy RiscV32IInstructionSetFactory
.
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
Następnie zdefiniuj zastąpienie dla CreateRiscv32Slot
. Ponieważ nie używamy żadnych
klasy derywowanej klasy Riscv32Slot
, po prostu przydzielamy nową instancję za pomocą argumentu
std::make_unique
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.
Zdefiniuj klasę dekodera
Deklaracje konstruktorów, destruktorów i metod
Teraz trzeba zdefiniować klasę dekodera. W tym samym pliku co powyżej otwórz
deklaracji RiscV32Decoder
. Rozwiń deklarację do definicji klasy
gdzie RiscV32Decoder
dziedziczy dane z generic::DecoderInterface
.
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
Następnie, zanim napiszemy konstruktor, przyjrzyjmy się kodowi
które wygenerujemy w drugim samouczku na dekoderze plików binarnych. Oprócz wszystkich
Extract
, istnieje funkcja DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Ta funkcja bierze pod uwagę słowo instrukcji, które musi zostać zdekodowane, i zwraca
zgodnie z instrukcją. Z drugiej strony
Klasa DecodeInterface
, którą RiscV32Decoder
implementuje tylko karty w
adresu. Dlatego klasa RiscV32Decoder
musi mieć dostęp do pamięci,
przeczytaj słowo z instrukcji do: DecodeRiscVInst32()
. W tym projekcie
dostępu do pamięci jest oparty na prostym interfejsie pamięci określonym
.../mpact/sim/util/memory
o odpowiedniej nazwie util::MemoryInterface
, zobacz poniżej:
// Load data from address into the DataBuffer, then schedule the Instruction
// inst (if not nullptr) to be executed (using the function delay line) with
// context. The size of the data access is based on size of the data buffer.
virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
ReferenceCount *context) = 0;
Dodatkowo musimy mieć możliwość przekazania instancji klasy state
do funkcji
konstruktory innych klas dekodera. Odpowiednia klasa stanu to
Klasa riscv::RiscVState
, która pochodzi od: generic::ArchState
, z dodanym
dla RiscV. Oznacza to, że musimy zadeklarować konstruktor, aby
może ustawić wskaźnik do state
i memory
:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
Usuń domyślny konstruktor i zastąp destruktor:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
Następnie zadeklaruj metodę DecodeInstruction
, którą musimy zastąpić
generic::DecoderInterface
generic::Instruction *DecodeInstruction(uint64_t address) override;
Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.
Definicje użytkowników danych
Klasa RiscV32Decoder
będzie potrzebować prywatnych członków danych, aby przechowywać
z parametrami konstruktora i wskaźnikiem do klasy fabryki.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
Wymaga też wskaźnika do klasy kodowania, która pochodzi z
RiscV32IEncodingBase
, nazwijmy ją RiscV32IEncoding
(wprowadzimy
w ćwiczeniu 2). Dodatkowo wymaga wskaźnika do instancji
RiscV32IInstructionSet
, więc dodaj:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
Na koniec musimy zdefiniować użytkownika danych do użytku w naszym interfejsie pamięci:
generic::DataBuffer *inst_db_;
Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.
Zdefiniuj metody klasy dekodera
Następnie trzeba wdrożyć konstruktor, destruktor
Metoda DecodeInstruction
. Otwórz plik riscv32_decoder.cc
. Wartość jest pusta
są już w pliku, a także deklaracje przestrzeni nazw i kilka metod
z using
deklaracji.
Definicja konstruktora
Konstruktor musi tylko zainicjować elementy danych. Najpierw zainicjuj
state_
i memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
Następnie przydziel instancje każdej z klas powiązanych z dekoderem, przekazując w poleceniu za pomocą odpowiednich parametrów.
// Allocate the isa factory class, the top level isa decoder instance, and
// the encoding parser.
riscv_isa_factory_ = new RiscV32IsaFactory();
riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
riscv_encoding_ = new RiscV32IEncoding(state);
Na koniec przydziel instancję DataBuffer
. Jest przydzielany z użyciem fabryki
dostępne w ramach usługi state_
. Przydzielamy bufor danych o rozmiarze umożliwiającym przechowywanie
pojedynczy uint32_t
, bo taki jest rozmiar słowa instrukcji.
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
Definicja destruktora
Destruktor jest prosty: wystarczy zwolnić obiekty przydzielone w konstruktorze,
ale jednym zakręciem. Instancja bufora danych jest liczona odniesieniem, więc zamiast tego
przy wywołaniu delete
dla tego wskaźnika DecRef()
obiekt:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
Definicja metody
W naszym przypadku wdrożenie tej metody jest dość proste. Przyjmijmy, że adres jest prawidłowo dopasowany i nie są sprawdzane żadne dodatkowe błędy.
Najpierw słowo instrukcji musi zostać pobrane z pamięci przy użyciu pamięci.
interfejsu i instancji DataBuffer
.
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
Następnie wywołujemy wystąpienie RiscVIEncoding
, aby przeanalizować słowo instrukcji,
co trzeba zrobić przed wywołaniem dekodera ISA. Pamiętaj, że agencja ISA
dekoder wywołuje bezpośrednio instancję RiscVIEncoding
, aby uzyskać kod op
i operandów określonych przez słowo instruktażowe. Nie stosujemy jeszcze tej metody
jeszcze nie ma klasy, ale użyjmy void ParseInstruction(uint32_t)
jako metody tej metody.
riscv_encoding_->ParseInstruction(iword);
Na koniec wywołujemy dekoder ISA, przekazując adres i klasę kodowania.
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.
Klasa kodowania
Klasa kodowania implementuje interfejs używany przez klasę dekodera uzyskać kod operacji instrukcji oraz jej operandy źródłowe i docelowe; operandów zasobów. Wszystkie te obiekty zależą od informacji z pliku binarnego z dekodera formatu, takiego jak kod operacji, wartości określonych pól w funkcji słowa instrukcji itp. Jest ona oddzielona od klasy dekodera, aby została niezależne od kodowania i obsługujące wiele różnych schematów kodowania w przyszłości.
RiscV32IEncodingBase
to klasa abstrakcyjna. Zestaw metod, które stosujemy,
można zastosować do klasy derywowanej poniżej.
class RiscV32IEncodingBase {
public:
virtual ~RiscV32IEncodingBase() = default;
virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;
virtual ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end) = 0;
virtual ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end) = 0;
virtual PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op) = 0;
virtual SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no) = 0;
virtual DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency) = 0;
virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no) = 0;
};
Na pierwszy rzut oka wydaje się to dość skomplikowane, zwłaszcza przy ale w przypadku prostej architektury, takiej jak RiscV, ignorujemy większość ponieważ ich wartości będą domniemane.
Omówmy po kolei wszystkie metody.
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
Metoda GetOpcode
zwraca element OpcodeEnum
dla bieżącego
określający kod opcyjny instrukcji. Zajęcia OpcodeEnum
to
zdefiniowane w wygenerowanym pliku dekodera isa riscv32i_enums.h
. Metoda
dwóch parametrów, z których każdy może być ignorowany. Pierwsza wartość
to jest typ przedziału (klasa wyliczeniowa zdefiniowana w riscv32i_enums.h
),
który, ponieważ RiscV ma tylko jeden boks, ma tylko jedną możliwą wartość:
SlotEnum::kRiscv32
Drugi to numer instancji przedziału (w przypadku
występuje wiele wystąpień przedziału, co może wystąpić w niektórych VLIW.
architektury).
ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end)
ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end);
Następne 2 metody są używane do modelowania zasobów sprzętowych procesora
aby poprawić dokładność cyklu. W ćwiczeniach instruktażowych nie będziemy używać
więc w implementacji zostaną one skrócone, zwracając nullptr
.
PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op);
SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no);
DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency);
Te trzy metody zwracają wskaźniki do obiektów operand używanych w obrębie
funkcji semantycznych instrukcji w celu uzyskania dostępu do wartości dowolnej instrukcji.
operand predykatu, każdego operandu źródła instrukcji i wpisać nowy
do operandów miejsca docelowego instrukcji. Ponieważ RiscV nie używa
predykatów instrukcji, ta metoda musi zwracać tylko nullptr
.
Wzorzec parametrów jest podobny we wszystkich tych funkcjach. Po pierwsze, podobnie jak
GetOpcode
boks i wpis są przekazywane. Następnie kod operacji dla operatora
instrukcja, dla której ma zostać utworzony operand. Jest ona używana tylko wtedy, gdy
różne kody operacji muszą zwracać różne obiekty operandu dla tego samego operandu
co nie jest możliwe w przypadku tego symulatora RiscV.
Dalej mamy wpis predykat, źródło, miejsce docelowe i wyliczenie operandów, które
wskazuje operand, który ma zostać utworzony. Pochodzą one z 3 grup,
OpEnums w riscv32i_enums.h
, tak jak poniżej:
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Jeśli spojrzysz na dane
riscv32.isa
pliku, zauważysz, że odpowiadają one zbiorom danych źródłowych i docelowych
nazwy operandów używanych w deklaracji poszczególnych instrukcji. Zastosowanie różnych opcji
nazwy operandów operandów, które reprezentują różne pola bitowe i operandy
typów kodu, ułatwia to pisanie klasy kodowania, ponieważ element enum jest unikalny
określa dokładny typ operandu do zwrócenia, dzięki czemu nie trzeba
uwzględnij wartości parametrów przedziału, wpisu lub kodu operacji.
W przypadku operandów źródłowych i docelowych jest to również położenie porządkowe funkcji operand jest przekazywany (znowu możemy go zignorować), a dla elementu docelowego operand, czas oczekiwania (w cyklach) upływający między momentem zastosowania instrukcji a wynik docelowy jest dostępny dla kolejnych instrukcji. W naszym symulatorze to opóźnienie wynosi 0, co oznacza, że instrukcja zapisuje wyniki są natychmiast przekazywane do rejestru.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
Ostatnia funkcja służy do pobierania czasu oczekiwania dla danego miejsca docelowego.
operand, jeśli został określony w pliku .isa
jako *
. Jest to rzadkie zjawisko,
i nie jest używana w tym symulatorze RiscV, więc nasza implementacja tej funkcji
zwróci po prostu 0.
Zdefiniuj klasę kodowania
Plik nagłówka (.h)
Metody
Otwórz plik riscv32i_encoding.h
. Wszystkie niezbędne pliki „Uwzględnij” zostały
już dodano, a przestrzenie nazw zostały skonfigurowane. Całe dodawanie kodu to
użytkownik obserwuje komentarz // Exercise 2.
Zacznijmy od zdefiniowania klasy RiscV32IEncoding
, która dziedziczy z klasy
za pomocą wygenerowanego interfejsu.
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
Następnie konstruktor powinien wskazać wskaźnik do instancji stanu (w tym przypadku będzie to stan)
wskaźnik do riscv::RiscVState
. Należy użyć domyślnego niszczyciela.
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
Zanim dopiszemy wszystkie metody interfejsu, dodajmy metodę wywoływaną przez
RiscV32Decoder
, aby przeanalizować instrukcję:
void ParseInstruction(uint32_t inst_word);
Teraz dodajmy metody, które mają proste zastąpienia i pozbądźmy nazwy nieużywanych parametrów:
// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
SimpleResourceVector &,
int) override {
return nullptr;
}
ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
ComplexResourceEnum ,
int, int) override {
return nullptr;
}
PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
PredOpEnum) override {
return nullptr;
}
int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }
Na koniec dodaj pozostałe zastąpienia metody interfejsu publicznego, ale z wdrożeniami odroczonymi do pliku .cc.
OpcodeEnum GetOpcode(SlotEnum, int) override;
SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
SourceOpEnum source_op, int) override;
DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
DestOpEnum dest_op, int,
int latency) override;
Aby uprościć implementację poszczególnych metod pobierania operandów
utworzymy dwie tablice wywołań (obiektów funkcji) indeksowanych przez metodę
wartości liczbowe odpowiednio SourceOpEnum
i DestOpEnum
.
W ten sposób formy są zredukowane do wywoływania
obiekt funkcji dla wartości wyliczenia, która jest przekazywana i zwracająca zwracaną wartość
.
Aby zorganizować inicjalizację tych dwóch tablic, definiujemy 2 prywatne które są wywoływane z konstruktora w następujący sposób:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
Użytkownicy danych
Wymagani użytkownicy danych:
state_
, aby przechowywać wartośćriscv::RiscVState *
.inst_word_
typuuint32_t
, który zawiera wartość bieżącej słowo instruktażowe.opcode_
, aby przechowywać kod operacji bieżącej instrukcji, który jest zaktualizowany przez metodyParseInstruction
. Ten typ zasobu zawiera:OpcodeEnum
.source_op_getters_
tablica do przechowywania wywołań używanych do uzyskania źródła obiektów operandu. Typ elementów tablicy toabsl::AnyInvocable<SourceOperandInterface *>()>
dest_op_getters_
tablica do przechowywania wywołań używanych do uzyskania obiekty operandu docelowego. Typ elementów tablicy toabsl::AnyInvocable<DestinationOperandInterface *>()>
xreg_alias
tablica nazw ABI rejestru liczb całkowitych RiscV, np. „zero” oraz „ra” zamiast „x0” i „x1”.
riscv::RiscVState *state_;
uint32_t inst_word_;
OpcodeEnum opcode_;
absl::AnyInvocable<SourceOperandInterface *()>
source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
absl::AnyInvocable<DestinationOperandInterface *(int)>
dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];
const std::string xreg_alias_[32] = {
"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0",
"a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5",
"s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};
Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.
Plik źródłowy (.cc).
Otwórz plik riscv32i_encoding.cc
. Wszystkie niezbędne pliki „Uwzględnij” zostały
już dodano, a przestrzenie nazw zostały skonfigurowane. Całe dodawanie kodu to
użytkownik obserwuje komentarz // Exercise 2.
Funkcje pomocnicze
Zaczniemy od opisania kilku funkcji pomocniczych, których używamy do tworzenia
operandy rejestru źródłowego i docelowego. Zostaną one utworzone na podstawie
rejestru i wywołuje obiekt RiscVState
, aby pobrać uchwyt dla
rejestrować obiekt, a następnie wywoływać metodę fabryki operandów w obiekcieregister.
Zacznijmy od pomocników docelowego operandu:
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency);
}
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency, op_name);
}
Jak widać, istnieją 2 funkcje pomocnicze. Drugi etap wymaga dodatkowych
parametr op_name
, który do operandu może mieć inną nazwę lub inny ciąg znaków
niż w rejestrze bazowym.
Podobnie w przypadku pomocników operandu źródłowego:
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand();
return op;
}
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand(op_name);
return op;
}
Funkcje konstruktora i interfejsu
Funkcje konstruktora i interfejsu są bardzo proste. Konstruktor wywołuje tylko 2 metody inicjowania tablic callables dla pobierających operandy.
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
przechowuje słowo instrukcji, a następnie podany kod operacji
uzyskanych dzięki wywołaniu kodu wygenerowanego przez dekoder binarny.
// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
inst_word_ = inst_word;
opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}
Moduły pobierające operandy zwracają wartość wywołaną przez funkcję pobierania .
DestinationOperandInterface *RiscV32IEncoding::GetDestination(
SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
return dest_op_getters_[static_cast<int>(dest_op)](latency);
}
SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
SourceOpEnum source_op, int) {
return source_op_getters_[static_cast<int>(source_op)]();
}
Metody inicjowania tablicy
Jak możesz się domyślić, większość pracy polega na inicjowaniu narzędzia
ale bez obaw – robi się to przy użyciu łatwego, powtarzającego się wzorca. Zacznijmy
zaczyna się od InitializeDestinationOpGetters()
, ponieważ istnieje tylko
kilka operandów miejsca docelowego.
Wycofaj wygenerowaną klasę DestOpEnum
z riscv32i_enums.h
:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
W przypadku dest_op_getters_
musimy zainicjować 4 wpisy, po jednym dla kNone
,
kCsr
, kNextPc
i kRd
. Dla wygody każdy wpis jest inicjowany ciągiem
lambda, ale możesz też użyć dowolnej innej formy wywołania. Podpis
wartości lambda wynosi void(int latency)
.
Do tej pory nie mówiliśmy zbyt wiele o różnych rodzajach miejsc docelowych,
operandy zdefiniowane w formacie MPACT-Sim. W tym ćwiczeniu użyjemy tylko dwóch
typy: generic::RegisterDestinationOperand
zdefiniowane w
register.h
,
i generic::DevNullOperand
zdefiniowane w
devnull_operand.h
Szczegóły tych operandów nie są obecnie szczególnie ważne, z wyjątkiem tego, że
pierwszy jest używany do zapisu w rejestrach, a drugi ignoruje wszystkie zapisy.
Pierwsza pozycja dla kNone
jest trywialna – wystarczy zwrócić wartość nullptr i opcjonalnie
zarejestrować błąd.
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
Kolejny serwis to kCsr
. Zrobimy tu trochę oszustw. „Witaj świecie” program
nie wymaga faktycznej aktualizacji żądania podpisania certyfikatu, ale zawiera
wykonaj instrukcje żądania podpisania certyfikatu. Rozwiązaniem jest pozbycie się tego problemu za pomocą
zwykły rejestr o nazwie „CSR” i kierować do niego wszystkie tego typu zapisy.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
Następna jest nazwa kNextPc
i odnosi się do nazwy „PC” rejestracji. Jest używana jako wartość docelowa
dla wszystkich instrukcji dotyczących gałęzi i skoków. Nazwa jest określona w języku: RiscVState
jako
kPcName
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
Na koniec znajduje się operand miejsca docelowego kRd
. W riscv32i.isa
operand
Parametr rd
jest używany wyłącznie do odwoływania się do rejestru całkowitej zakodowanego w „rd” pole
wyrazu instruktażowego, więc nie ma wątpliwości, do której się odnosi. OK
to tylko jeden widżet. Rejestracja domeny x0
(nazwa abi zero
) jest na stałe ustawiona na 0,
W tym rejestrze korzystamy z DevNullOperand
.
W tym narzędziu getter najpierw wyodrębniamy wartość z pola rd
za pomocą funkcji
Metoda Extract
wygenerowana z pliku .bin_fmt. Jeśli wartość wynosi 0,
zwraca „DevNull”, operand. W przeciwnym razie zwracamy poprawny operand rejestru,
pamiętając o użyciu odpowiedniego aliasu rejestru jako nazwy operandu.
dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
// First extract register number from rd field.
int num = inst32_format::ExtractRd(inst_word_);
// For register x0, return the DevNull operand.
if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
// Return the proper register operand.
return GetRegisterDestinationOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
xreg_alias_[num]);
)
}
}
Przejdźmy do metody InitializeSourceOperandGetters()
, w której wzór
prawie taki sam, ale szczegóły nieco się różnią.
Najpierw przyjrzymy się elementowi SourceOpEnum
wygenerowanym na podstawie
riscv32i.isa
w pierwszym samouczku:
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
Oprócz grupy kNone
członkowie dzielą się na 2 grupy. Jeden
jest bezpośrednimi operandami: kBimm12
, kImm12
, kJimm20
, kSimm12
, kUimm20
,
i kUimm5
. Drugie to operandy rejestru: kCsr
, kRs1
i kRs2
.
Argument kNone
jest obsługiwany tak samo jak w przypadku operandów miejsca docelowego – zwracaj
nullptr.
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
Teraz popracujemy nad operandami rejestru. Zajmiemy się kCsr
podobnymi
do sposobu obsługi odpowiednich operandów miejsca docelowego – po prostu wywołaj funkcję
funkcja pomocnicza używająca żądania „CSR” jako nazwę rejestru.
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
Operatory kRs1
i kRs2
są obsługiwane tak samo jak kRd
, z tym wyjątkiem
nie chcieliśmy aktualizować rozszerzenia x0
(lub zero
), ale chcemy upewnić się,
zawsze odczytuje z tego operandu 0. Użyjemy do tego celu
generic::IntLiteralOperand<>
klasa zdefiniowana w
literal_operand.h
Ten operand służy do przechowywania wartości literału (w przeciwieństwie do symulowanej wartości
wartość natychmiastową). Jeśli tego nie zrobisz, wzór będzie taki sam: najpierw wyodrębnij
Wartość rs1/rs2 ze słowa instrukcji, jeśli wartość wynosi zero, zwraca literał
operand z parametrem szablonu o wartości 0, w przeciwnym razie zwraca zwykły rejestr
operand źródłowy przy użyciu funkcji pomocniczej z aliasem abi jako operandem
imię i nazwisko.
source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs1(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs2(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
Na koniec obsługujemy różne natychmiastowe operandy. Wartości natychmiastowe to
przechowywane w instancjach klasy generic::ImmediateOperand<>
zdefiniowanej w
immediate_operand.h
Jedyna różnica między różnymi metodami getter w przypadku bezpośrednich operandów.
wskazuje, która funkcja wyodrębniania jest używana i czy typ pamięci masowej jest podpisany lub
zgodnie z polem bitowym.
// Immediates.
source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractBImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractImm12(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm5(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractJImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractSImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm32(inst_word_));
};
}
Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.
To koniec samouczka. Mamy nadzieję, że te informacje okażą się przydatne.