Tujuan tutorial ini adalah:
- Pelajari cara ISA dan dekoder biner yang dihasilkan cocok satu sama lain.
- Tulis kode C++ yang diperlukan untuk membuat dekoder petunjuk lengkap untuk RiscV RV32I yang menggabungkan dekoder ISA dan biner.
Memahami dekoder petunjuk
Decoder instruksi bertanggung jawab untuk, dengan alamat instruksi, membaca kata petunjuk dari memori dan menampilkan instance Instruction
yang diinisialisasi sepenuhnya yang merepresentasikan instruksi tersebut.
Decoder tingkat atas menerapkan generic::DecoderInterface
yang ditampilkan di bawah ini:
// 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;
};
Seperti yang dapat Anda lihat, hanya ada satu metode yang harus diimplementasikan: cpp
virtual Instruction *DecodeInstruction(uint64_t address);
Sekarang, mari kita lihat apa yang disediakan dan apa yang diperlukan oleh kode yang dihasilkan.
Pertama, pertimbangkan class level teratas RiscV32IInstructionSet
dalam file
riscv32i_decoder.h
, yang dibuat di akhir tutorial tentang
decoder ISA. Untuk melihat konten yang baru, buka direktori solusi dari tutorial tersebut dan buat ulang semuanya.
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
Sekarang, ubah direktori Anda kembali ke root repositori, lalu mari kita lihat
sumber yang dihasilkan. Untuk itu, ubah direktori ke
bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(dengan asumsi Anda menggunakan host
x86 - untuk host lain, k8-fastbuild akan menjadi string lain).
$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
Anda akan melihat empat file sumber yang berisi kode C++ yang dihasilkan:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Buka file pertama riscv32i_decoder.h
. Ada tiga class yang perlu kita lihat:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
Perhatikan penamaan class. Semua class diberi nama berdasarkan
versi Pascal-case dari nama yang diberikan dalam deklarasi "isa" dalam file tersebut:
isa RiscV32I { ... }
Mari kita mulai dengan class RiscVIInstructionSet
terlebih dahulu. Hal ini ditampilkan di bawah ini:
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_;
};
Tidak ada metode virtual di class ini, sehingga ini adalah class mandiri, tetapi
perhatikan dua hal. Pertama, konstruktor mengambil pointer ke instance
class RiscV32IInstructionSetFactory
. Ini adalah class yang digunakan dekoder
yang dihasilkan untuk membuat instance class RiscV32Slot
, yang digunakan untuk
mendekode semua petunjuk yang ditentukan untuk slot RiscV32
seperti yang didefinisikan dalam
file riscv32i.isa
. Kedua, metode Decode
mengambil parameter tambahan
jenis pointer ke RiscV32IEncodingBase
, ini adalah class yang akan menyediakan
antarmuka antara decoder isa yang dihasilkan dalam tutorial pertama dan decoder
biner yang dihasilkan di lab kedua.
Class RiscV32IInstructionSetFactory
adalah class abstrak yang harus kita
dapatkan implementasinya sendiri untuk dekoder lengkap. Dalam sebagian besar kasus, class
ini bersifat sepele: cukup berikan metode untuk memanggil konstruktor untuk setiap
class slot yang ditentukan dalam file .isa
. Dalam kasus kita, ini sangat sederhana karena
hanya ada satu class tersebut: Riscv32Slot
(Pascal-case dari nama riscv32
yang digabungkan dengan Slot
). Metode ini tidak dibuat untuk Anda karena ada
beberapa kasus penggunaan lanjutan yang mungkin memiliki utilitas dalam memperoleh subclass
dari slot, dan memanggil konstruktornya.
Kita akan membahas class terakhir RiscV32IEncodingBase
nanti dalam tutorial
ini, karena ini merupakan subjek latihan lain.
Menentukan decoder petunjuk tingkat teratas
Menentukan class factory
Jika Anda mem-build ulang project untuk tutorial pertama, pastikan Anda kembali ke
direktori riscv_full_decoder
.
Buka file riscv32_decoder.h
. Semua file include yang diperlukan telah
ditambahkan dan namespace telah disiapkan.
Setelah komentar bertanda //Exercise 1 - step 1
, tentukan class
RiscV32IsaFactory
yang mewarisi dari RiscV32IInstructionSetFactory
.
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
Selanjutnya, tentukan penggantian untuk CreateRiscv32Slot
. Karena tidak menggunakan
class turunan Riscv32Slot
, kita cukup mengalokasikan instance baru menggunakan
std::make_unique
.
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
Jika Anda memerlukan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya ada di sini.
Menentukan class decoder
Deklarasi konstruktor, destruktor, dan metode
Selanjutnya, saatnya menentukan class decoder. Dalam file yang sama seperti di atas, buka
deklarasi RiscV32Decoder
. Luaskan deklarasi menjadi definisi class
tempat RiscV32Decoder
mewarisi dari generic::DecoderInterface
.
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
Selanjutnya, sebelum menulis konstruktor, mari kita lihat sekilas kode yang dihasilkan dalam tutorial kedua tentang decoder biner. Selain semua
fungsi Extract
, ada fungsi DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Fungsi ini menggunakan kata petunjuk yang perlu didekode, dan menampilkan
opcode yang cocok dengan petunjuk tersebut. Di sisi lain, class DecodeInterface
yang diimplementasikan RiscV32Decoder
hanya meneruskan
alamat. Dengan demikian, class RiscV32Decoder
harus dapat mengakses memori untuk
membaca kata petunjuk yang akan diteruskan ke DecodeRiscVInst32()
. Dalam project ini,
cara mengakses memori adalah melalui antarmuka memori sederhana yang ditentukan di
.../mpact/sim/util/memory
yang dinamai dengan tepat util::MemoryInterface
, yang terlihat di bawah:
// 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;
Selain itu, kita harus dapat meneruskan instance class state
ke
konstruktor class decoder lainnya. Class status yang sesuai adalah
class riscv::RiscVState
, yang berasal dari generic::ArchState
, dengan fungsi
tambahan untuk RiscV. Artinya, kita harus mendeklarasikan konstruktor agar
dapat mengambil pointer ke state
dan memory
:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
Hapus konstruktor default dan ganti destruktor:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
Selanjutnya, deklarasikan metode DecodeInstruction
yang perlu kita ganti dari
generic::DecoderInterface
.
generic::Instruction *DecodeInstruction(uint64_t address) override;
Jika Anda memerlukan bantuan (atau ingin memeriksa hasil pekerjaan Anda), jawaban lengkapnya ada di sini.
Definisi Anggota Data
Class RiscV32Decoder
akan memerlukan anggota data pribadi untuk menyimpan
parameter konstruktor dan pointer ke class factory.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
Class ini juga memerlukan pointer ke class encoding yang berasal dari
RiscV32IEncodingBase
, sebut saja RiscV32IEncoding
(kita akan menerapkannya
dalam latihan 2). Selain itu, pointer memerlukan instance
RiscV32IInstructionSet
, jadi tambahkan:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
Terakhir, kita perlu menentukan anggota data untuk digunakan dengan antarmuka memori:
generic::DataBuffer *inst_db_;
Jika Anda memerlukan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya ada di sini.
Menentukan Metode Class Decoder
Selanjutnya, saatnya mengimplementasikan konstruktor, destruktor, dan
metode DecodeInstruction
. Buka file riscv32_decoder.cc
. Metode kosong
sudah ada dalam file serta deklarasi namespace dan beberapa
deklarasi using
.
Definisi Konstruktor
Konstruktor hanya perlu menginisialisasi anggota data. Pertama, lakukan inisialisasi
state_
dan memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
Selanjutnya, alokasikan instance dari setiap class terkait decoder, dengan meneruskan parameter yang sesuai.
// 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);
Terakhir, alokasikan instance DataBuffer
. Ini dialokasikan menggunakan factory
yang dapat diakses melalui anggota state_
. Kita mengalokasikan buffer data yang berukuran untuk menyimpan
satu uint32_t
, karena itu adalah ukuran kata petunjuk.
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
Definisi Destruktor
Destruktor itu sederhana, cukup bebaskan objek yang kita alokasikan di konstruktor,
tetapi dengan satu putaran. Instance buffering data dihitung referensi, jadi sebagai gantinya,
bukan memanggil delete
pada pointer tersebut, kita DecRef()
objek:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
Definisi metode
Dalam kasus kita, implementasi metode ini cukup sederhana. Kita akan mengasumsikan bahwa alamat sudah disejajarkan dengan benar dan tidak diperlukan pemeriksaan error tambahan.
Pertama, kata petunjuk harus diambil dari memori menggunakan antarmuka
memori dan instance DataBuffer
.
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
Selanjutnya, kita memanggil instance RiscVIEncoding
untuk mengurai kata petunjuk,
yang harus dilakukan sebelum memanggil dekoder ISA itu sendiri. Ingat bahwa decoder
ISA memanggil instance RiscVIEncoding
secara langsung untuk mendapatkan opcode
dan operand yang ditentukan oleh kata petunjuk. Kita belum mengimplementasikan class
tersebut, tetapi mari kita gunakan void ParseInstruction(uint32_t)
sebagai metode tersebut.
riscv_encoding_->ParseInstruction(iword);
Terakhir, kita memanggil decoder ISA, yang meneruskan alamat dan class Encoding.
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
Jika Anda memerlukan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya ada di sini.
Class encoding
Class encoding mengimplementasikan antarmuka yang digunakan oleh class decoder untuk mendapatkan opcode petunjuk, operand sumber dan tujuannya, serta operand resource. Semua objek ini bergantung pada informasi dari decoder format biner, seperti opcode, nilai kolom tertentu dalam kata petunjuk, dll. Ini dipisahkan dari class decoder agar tetap mengenkodenya agnostik dan mengaktifkan dukungan untuk berbagai skema encoding yang berbeda di masa mendatang.
RiscV32IEncodingBase
adalah class abstrak. Serangkaian metode yang harus kita
implementasikan di class turunan ditunjukkan di bawah ini.
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;
};
Sekilas, ini terlihat agak rumit, terutama dengan jumlah parameter, tetapi untuk arsitektur sederhana seperti RiscV, kita sebenarnya mengabaikan sebagian besar parameter, karena nilainya akan tersirat.
Mari kita bahas setiap metodenya satu per satu.
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
Metode GetOpcode
menampilkan anggota OpcodeEnum
untuk
instruksi saat ini, yang mengidentifikasi opcode petunjuk. Class OpcodeEnum
ditentukan dalam file decoder isa yang dihasilkan riscv32i_enums.h
. Metode ini mengambil
dua parameter, yang keduanya dapat diabaikan untuk tujuan kita. Yang pertama
adalah jenis slot (class enum yang juga ditentukan di riscv32i_enums.h
),
yang, karena RiscV hanya memiliki satu slot, hanya memiliki satu kemungkinan nilai:
SlotEnum::kRiscv32
. Yang kedua adalah nomor instance slot (jika
ada beberapa instance slot, yang mungkin terjadi di beberapa arsitektur
VLIW).
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);
Dua metode berikutnya digunakan untuk memodelkan resource hardware dalam prosesor
guna meningkatkan akurasi siklus. Untuk latihan tutorial, kita tidak akan menggunakannya, sehingga dalam implementasi, stub akan dihapus, yang menampilkan 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);
Ketiga metode ini menampilkan pointer ke objek operand yang digunakan dalam
fungsi semantik petunjuk untuk mengakses nilai operand
predikat petunjuk, setiap operand sumber petunjuk, dan menulis nilai
baru ke operand tujuan petunjuk. Karena RiscV tidak menggunakan
predikat petunjuk, metode tersebut hanya perlu menampilkan nullptr
.
Pola parameter serupa di seluruh fungsi ini. Pertama, sama seperti
GetOpcode
, slot dan entri diteruskan. Kemudian, opcode untuk
instruksi yang mana operand harus dibuat. Hal ini hanya digunakan jika
opcode yang berbeda perlu menampilkan objek operand yang berbeda untuk jenis
operand yang sama, yang tidak berlaku untuk simulator RiscV ini.
Berikutnya adalah Predicate, Source, dan Destination, entri enumerasi operand yang
mengidentifikasi operand yang harus dibuat. Ini berasal dari tiga
OpEnums di riscv32i_enums.h
seperti yang terlihat di bawah ini:
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,
};
Jika melihat kembali file
riscv32.isa
,
Anda akan melihat bahwa file ini sesuai dengan kumpulan nama operand
sumber dan tujuan yang digunakan dalam deklarasi setiap petunjuk. Dengan menggunakan nama
operand yang berbeda untuk operand yang mewakili jenis operand dan bitfield
yang berbeda, penulisan class encoding menjadi lebih mudah karena anggota enum secara unik
menentukan jenis operand yang tepat untuk ditampilkan, dan tidak perlu
mempertimbangkan nilai parameter slot, entri, atau opcode.
Terakhir, untuk operand sumber dan tujuan, posisi ordinal operand diteruskan (sekali lagi, kita dapat mengabaikannya), dan untuk operand tujuan, latensi (dalam siklus) yang berlalu antara waktu perintah diterbitkan, dan hasil tujuan tersedia untuk petunjuk berikutnya. Dalam simulator kita, latensi ini akan menjadi 0, yang berarti bahwa petunjuk akan langsung menulis hasil ke register.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
Fungsi akhir digunakan untuk mendapatkan latensi operand tujuan tertentu jika telah ditentukan sebagai *
dalam file .isa
. Hal ini jarang terjadi,
dan tidak digunakan untuk simulator RiscV ini, sehingga implementasi fungsi ini
hanya akan menampilkan 0.
Menentukan class encoding
File header (.h)
Metode
Buka file riscv32i_encoding.h
. Semua file include yang diperlukan telah
ditambahkan dan namespace telah disiapkan. Semua penambahan kode
dilakukan setelah komentar // Exercise 2.
Mari kita mulai dengan menentukan class RiscV32IEncoding
yang mewarisi dari
antarmuka yang dihasilkan.
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
Selanjutnya, konstruktor harus mengambil pointer ke instance status, dalam hal ini
pointer ke riscv::RiscVState
. Destruktor default harus digunakan.
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
Sebelum menambahkan semua metode antarmuka, mari kita tambahkan metode yang dipanggil oleh
RiscV32Decoder
untuk mengurai petunjuk:
void ParseInstruction(uint32_t inst_word);
Selanjutnya, mari tambahkan metode yang memiliki penggantian trivial sambil menghapus nama parameter yang tidak digunakan:
// 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; }
Terakhir, tambahkan penggantian metode yang tersisa dari antarmuka publik, tetapi dengan implementasi yang ditangguhkan ke file .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;
Untuk menyederhanakan implementasi setiap metode pengambil operand,
kita akan membuat dua array callable (objek fungsi) yang diindeks oleh
nilai numerik masing-masing anggota SourceOpEnum
dan DestOpEnum
.
Dengan cara ini, isi dari kedua metode ini dikurangi menjadi memanggil
objek fungsi untuk nilai enum yang diteruskan dan menampilkan nilai
pengembaliannya.
Untuk mengatur inisialisasi kedua array ini, kita menentukan dua metode pribadi yang akan dipanggil dari konstruktor sebagai berikut:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
Anggota data
Anggota data yang diperlukan adalah sebagai berikut:
state_
untuk menyimpan nilairiscv::RiscVState *
.inst_word_
dari jenisuint32_t
yang menyimpan nilai kata petunjuk saat ini.opcode_
untuk menyimpan opcode dari petunjuk saat ini yang diperbarui oleh metodeParseInstruction
. Ini memiliki jenisOpcodeEnum
.source_op_getters_
array untuk menyimpan callable yang digunakan untuk mendapatkan objek operand sumber. Jenis elemen array adalahabsl::AnyInvocable<SourceOperandInterface *>()>
.dest_op_getters_
array untuk menyimpan callable yang digunakan untuk mendapatkan objek operand tujuan. Jenis elemen array adalahabsl::AnyInvocable<DestinationOperandInterface *>()>
.xreg_alias
array nama ABI register bilangan bulat RiscV, misalnya, "zero" dan "ra", bukan "x0" dan "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"};
Jika Anda memerlukan bantuan (atau ingin memeriksa hasil pekerjaan Anda), jawaban lengkapnya ada di sini.
File sumber (.cc).
Buka file riscv32i_encoding.cc
. Semua file include yang diperlukan telah
ditambahkan dan namespace telah disiapkan. Semua penambahan kode dilakukan
setelah komentar // Exercise 2.
Fungsi bantuan
Kita akan mulai dengan menulis beberapa fungsi bantuan yang kita gunakan untuk membuat
Operand daftar sumber dan tujuan. Fungsi ini akan dijadikan template pada
jenis register dan akan memanggil objek RiscVState
untuk mendapatkan handle bagi
objek register, lalu memanggil metode factory operand dalam objek register.
Mari kita mulai dengan helper operand tujuan:
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);
}
Seperti yang Anda lihat, ada dua fungsi bantuan. Yang kedua menggunakan parameter tambahan
op_name
yang memungkinkan operand memiliki nama, atau representasi
string, yang berbeda dari register yang mendasarinya.
Demikian pula untuk helper operand sumber:
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;
}
Fungsi konstruktor dan antarmuka
Konstruktor dan fungsi antarmuka sangat sederhana. Konstruktor hanya memanggil dua metode inisialisasi untuk menginisialisasi array callable untuk pengambil operand.
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
menyimpan kata petunjuk, lalu opcode yang diperolehnya
dari memanggil kode yang dihasilkan decoder biner.
// 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_);
}
Terakhir, pengambil operand menampilkan nilai dari fungsi pengambil yang dipanggilnya berdasarkan pencarian array menggunakan nilai enum operand tujuan/sumber.
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)]();
}
Metode inisialisasi array
Seperti yang mungkin Anda duga, sebagian besar pekerjaannya adalah melakukan inisialisasi array pengambil, tetapi jangan khawatir, ini dilakukan menggunakan pola berulang yang mudah. Mari kita
mulai dengan InitializeDestinationOpGetters()
terlebih dahulu, karena hanya ada
beberapa operand tujuan.
Ingat kembali class DestOpEnum
yang dihasilkan dari riscv32i_enums.h
:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Untuk dest_op_getters_
, kita perlu melakukan inisialisasi 4 entri, masing-masing untuk kNone
,
kCsr
, kNextPc
, dan kRd
. Untuk memudahkan, setiap entri diinisialisasi dengan
lambda, meskipun Anda juga dapat menggunakan bentuk callable lainnya. Tanda tangan
lambda adalah void(int latency)
.
Sampai sekarang, kita belum banyak membahas berbagai jenis operand
tujuan yang ditentukan di MPACT-Sim. Untuk latihan ini, kita hanya akan menggunakan dua
jenis: generic::RegisterDestinationOperand
yang ditentukan dalam
register.h
,
dan generic::DevNullOperand
yang ditentukan dalam
devnull_operand.h
.
Detail operand ini tidak terlalu penting saat ini, kecuali bahwa
yang pertama digunakan untuk menulis ke register, dan yang kedua mengabaikan semua operasi tulis.
Entri pertama untuk kNone
bersifat sepele - cukup tampilkan nullptr dan opsional
catat error ke dalam log.
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
Berikutnya adalah kCsr
. Di sini kita akan menipu sedikit. Program "hello world"
tidak bergantung pada update CSR yang sebenarnya, tetapi ada beberapa kode boilerplate yang
mengeksekusi petunjuk CSR. Solusinya adalah dengan menirunya menggunakan register reguler bernama "CSR" dan menyalurkan semua penulisan tersebut ke sana.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
Berikutnya adalah kNextPc
, yang mengacu pada register "pc". Ini digunakan sebagai target
untuk semua petunjuk cabang dan lompat. Nama ditentukan dalam RiscVState
sebagai
kPcName
.
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
Terakhir, ada operand tujuan kRd
. Dalam riscv32i.isa
operand
rd
hanya digunakan untuk merujuk ke register bilangan bulat yang dienkode di kolom "rd"
dari kata petunjuk, sehingga tidak ada ambiguitas yang dirujuknya. Hanya
ada satu detail. Register x0
(nama abi zero
) di-hardcode ke 0,
jadi untuk register tersebut, kita menggunakan DevNullOperand
.
Jadi dalam pengambil ini, kita terlebih dahulu mengekstrak nilai di kolom rd
menggunakan
metode Extract
yang dihasilkan dari file .bin_fmt. Jika nilainya 0, kita
akan menampilkan operand "DevNull", jika tidak, kita akan menampilkan operand register yang benar,
dengan berhati-hati menggunakan alias register yang sesuai sebagai nama operand.
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]);
)
}
}
Sekarang ke metode InitializeSourceOperandGetters()
, dengan pola yang
hampir sama, tetapi detailnya sedikit berbeda.
Pertama, mari kita lihat SourceOpEnum
yang dihasilkan dari
riscv32i.isa
dalam tutorial pertama:
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,
};
Selain kNone
, anggota tersebut terbagi menjadi dua grup. Satu
adalah operand langsung: kBimm12
, kImm12
, kJimm20
, kSimm12
, kUimm20
,
dan kUimm5
. Yang lainnya adalah operand register: kCsr
, kRs1
, dan kRs2
.
Operand kNone
ditangani seperti untuk operand tujuan - menampilkan
nullptr.
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
Selanjutnya, mari kita kerjakan operand register. Kita akan menangani kCsr
mirip
dengan cara menangani operand tujuan yang sesuai - cukup panggil
fungsi bantuan menggunakan "CSR" sebagai nama register.
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
Operand kRs1
dan kRs2
ditangani secara setara dengan kRd
, kecuali bahwa
meskipun kita tidak ingin memperbarui x0
(atau zero
), kita ingin memastikan bahwa
kita selalu membaca 0 dari operand tersebut. Untuk itu, kita akan menggunakan
class generic::IntLiteralOperand<>
yang ditentukan di
literal_operand.h
.
Operand ini digunakan untuk menyimpan nilai literal (bukan nilai langsung
yang disimulasikan). Jika tidak, polanya sama: pertama-tama ekstrak
nilai rs1/rs2 dari kata perintah, jika nol, tampilkan operand literal
dengan parameter template 0, jika tidak, tampilkan operand sumber register
reguler menggunakan fungsi bantuan, menggunakan alias abi sebagai nama
operand.
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]);
};
Terakhir, kita menangani berbagai operand langsung. Nilai langsung
disimpan dalam instance class generic::ImmediateOperand<>
yang ditentukan di
immediate_operand.h
.
Satu-satunya perbedaan antara berbagai pengambil untuk operand langsung
adalah fungsi Pengekstrak mana yang digunakan, dan apakah jenis penyimpanan ditandatangani atau
tidak ditandatangani, sesuai dengan bitfield.
// 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_));
};
}
Jika Anda memerlukan bantuan (atau ingin memeriksa hasil pekerjaan Anda), jawaban lengkapnya ada di sini.
Demikianlah tutorial ini. Semoga informasi ini bermanfaat.