本教學課程的目標如下:
- 瞭解產生的 ISA 和二進位解碼器如何相輔相成。
- 編寫必要的 C++ 程式碼,為 RiscV RV32I 建立完整指令解碼器,並結合 ISA 和二進位解碼器。
瞭解指令解碼器
指令解碼器會在收到指令位址後,從記憶體讀取指令字,並傳回代表該指令的 Instruction
完整初始化例項。
頂層解碼器會實作以下 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;
};
如您所見,只有一個方法需要實作:cpp
virtual Instruction *DecodeInstruction(uint64_t address);
接下來,我們來看看產生的程式碼提供哪些內容,以及需要哪些內容。
首先,請考慮檔案 riscv32i_decoder.h
中的頂層類別 RiscV32IInstructionSet
,這是在 ISA 解碼器教學課程結尾產生的。如要查看新內容,請前往該教學課程的解決方案目錄,然後重新建構所有內容。
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
現在請將目錄變更回存放區根目錄,然後查看產生的來源。為此,請將目錄變更為 bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(假設您使用的是 x86 主機 - 對於其他主機,k8-fastbuild 會是另一個字串)。
$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
您會看到四個包含產生 C++ 程式碼的來源檔案:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
開啟第一個檔案 riscv32i_decoder.h
。我們需要查看三個類別:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
請注意類別的命名方式。所有類別的命名方式皆以該檔案中「isa」宣告中提供的名稱的 Pascal 大小寫版本為準:
isa RiscV32I { ... }
我們先從 RiscVIInstructionSet
類別開始。如下所示:
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_;
};
這個類別中沒有任何虛擬方法,因此這是個獨立類別,但請注意兩件事。首先,建構函式會取得 RiscV32IInstructionSetFactory
類別例項的指標。這是產生的解碼器用來建立 RiscV32Slot
類別例項的類別,用於解碼 riscv32i.isa
檔案中為 slot RiscV32
定義的所有操作說明。其次,Decode
方法會採用 RiscV32IEncodingBase
型別指標的額外參數,這個類別會在第一個教學課程中產生的 isa 解碼器與第二個實驗室中產生的二進位解碼器之間提供介面。
RiscV32IInstructionSetFactory
類別是抽象類別,我們必須從中衍生完整解碼器的實作項目。在大多數情況下,這個類別很簡單:只要提供方法,即可為 .isa
檔案中定義的每個插槽類別呼叫建構函式。在我們的例子中,由於只有一個類別:Riscv32Slot
(Pascal 大小寫名稱 riscv32
與 Slot
連結),因此非常簡單。由於有些進階用途可能會從這個位置衍生出子類別,並改為呼叫其建構函式,因此系統不會為您產生這個方法。
本教學課程稍後會介紹最終類別 RiscV32IEncodingBase
,因為這是另一項練習的主題。
定義頂層指令解碼器
定義工廠類別
如果您為第一個教學課程重建專案,請務必改回 riscv_full_decoder
目錄。
開啟檔案 riscv32_decoder.h
。所有必要的包含檔案都已新增,命名空間也已設定。
在註解標示為 //Exercise 1 - step 1
之後,定義繼承自 RiscV32IInstructionSetFactory
的 RiscV32IsaFactory
類別。
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
接著,定義 CreateRiscv32Slot
的覆寫值。由於我們不會使用任何 Riscv32Slot
的衍生類別,因此我們只需使用 std::make_unique
分配新的例項。
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
如需協助 (或想檢查自己的答案),請參閱這篇文章的完整解答。
定義解碼器類別
建構函式、解構函式和方法宣告
接下來,我們要定義解碼器類別。在與上述同一個檔案中,前往 RiscV32Decoder
的宣告。將宣告擴充為類別定義,其中 RiscV32Decoder
會繼承自 generic::DecoderInterface
。
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
接著,開始編寫建構函式之前,讓我們快速觀看第二個有關二進位檔解碼器教學課程產生的程式碼。除了所有 Extract
函式外,還有 DecodeRiscVInst32
函式:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
這個函式會取用需要解碼的指令字,並傳回與該指令相符的 Opcode。另一方面,RiscV32Decoder
實作的 DecodeInterface
類別只會傳入位址。因此,RiscV32Decoder
類別必須能夠存取記憶體,才能讀取要傳遞至 DecodeRiscVInst32()
的指令字。在這個專案中,存取記憶體的方式是透過 .../mpact/sim/util/memory
中定義的簡單記憶體介面,其名稱為 util::MemoryInterface
,如下所示:
// 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;
此外,我們需要能夠將 state
類別例項傳遞至其他解碼器類別的建構函式。適當的狀態類別是 riscv::RiscVState
類別,可從 generic::ArchState
衍生,並新增 RiscV 的功能。這表示我們必須宣告建構函式,使其指向 state
和 memory
:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
刪除預設建構函式並覆寫解構函式:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
接著,請宣告需要從 generic::DecoderInterface
覆寫的 DecodeInstruction
方法。
generic::Instruction *DecodeInstruction(uint64_t address) override;
如需協助 (或想檢查自己的答案),請參閱這篇文章的完整解答。
資料成員定義
RiscV32Decoder
類別需要私人資料成員來儲存建構函式參數和工廠類別的指標。
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
這也需要指標指向衍生自 RiscV32IEncodingBase
的編碼類別,我們就要呼叫 RiscV32IEncoding
(我們會在練習 2 中實作此做法)。此外,它需要指向 RiscV32IInstructionSet
例項的指標,因此請新增:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
最後,我們需要定義資料成員,以便與記憶體介面搭配使用:
generic::DataBuffer *inst_db_;
如需協助 (或想檢查自己的答案),請參閱這篇文章的完整解答。
定義解碼器類別方法
接下來,請實作建構函式、解構函式和 DecodeInstruction
方法。開啟檔案 riscv32_decoder.cc
。空白方法已在檔案中,以及命名空間宣告和幾個 using
宣告。
建構函式定義
建構函式只需要初始化資料成員。首先初始化 state_
和 memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
接著,請為每個解碼器相關類別的例項分配例項,並傳入適當的參數。
// 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);
最後,請分配 DataBuffer
例項。該配置是使用可透過 state_
成員存取的工廠進行分配。我們會分配一個資料緩衝區,大小為單一 uint32_t
,因為這是指令字的大小。
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
解構函式定義
析構函式很簡單,只需釋放我們在建構函式中分配的物件即可,但有一個小變化。資料緩衝區例項在計算範圍內,因此在該指標上呼叫 delete
時,我們會 DecRef()
物件:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
方法定義
在這個案例中,這個方法的實作方式非常簡單。我們會假設地址正確對齊,因此不需要額外檢查錯誤。
首先,必須使用記憶體介面和 DataBuffer
例項,從記憶體中擷取指令字。
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
接下來,我們會呼叫 RiscVIEncoding
例項,以便剖析指令字,但必須在呼叫 ISA 解碼器之前完成。請回想,ISA 解碼器會直接呼叫 RiscVIEncoding
例項,以取得指令字指定的運算碼和運算元。我們尚未實作該類別,但請使用 void ParseInstruction(uint32_t)
做為該方法。
riscv_encoding_->ParseInstruction(iword);
最後,我們會呼叫 ISA 解碼器,並傳入地址和 Encoding 類別。
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
如需協助 (或想檢查自己的答案),請參閱這篇文章的完整解答。
編碼類別
編碼類別會實作由解碼器類別使用的介面,以取得指令運算碼、來源和目的地運算元,以及資源運算元。這些物件全都依賴二進位格式解碼器的資訊,例如運算碼、指示字詞中特定欄位的值等。並且與解碼器類別區隔開來,保持編碼通用,日後支援多種不同的編碼配置。
RiscV32IEncodingBase
是抽象類別。我們必須在衍生類別中實作的一系列方法如下所示。
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;
};
乍看之下,這似乎有點複雜,尤其是參數的數量,但對於 RiscV 這類簡單的架構,我們實際上會忽略大部分參數,因為這些參數的值會隱含在其中。
接下來將逐一介紹各個方法。
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
GetOpcode
方法會傳回目前指令的 OpcodeEnum
成員,以識別指令運算碼。產生的 Ia 解碼器檔案 riscv32i_enums.h
中會定義 OpcodeEnum
類別。此方法會採用兩個參數,但這兩個參數在我們這裡可以忽略。第一個是插槽類型 (列舉類別,也在 riscv32i_enums.h
中定義),由於 RiscV 只有一個插槽,因此只有一個可能的值:SlotEnum::kRiscv32
。第二個是運算單元的執行個體編號 (如果該運算單元有多個執行個體,這可能會在某些 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);
下列兩種方法可用於模擬處理器中的硬體資源,以提高週期準確度。在教學課程練習中,我們不會使用這些值,因此在實作時,這些值會被暫存,並傳回 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);
這三種方法會傳回指標至運算子物件,這些物件會在指令語意函式中使用,用於存取任何指令判定運算子、每個指令來源運算子的值,並將新值寫入指令目的地運算子。由於 RiscV 不會使用指令前置詞,因此該方法只需傳回 nullptr
。
這些函式中的參數模式都很類似。首先,就像 GetOpcode
一樣,會傳入插槽和項目。接著是運算元必須建立的操作碼指令。只有在不同的運算碼需要針對相同運算元類型傳回不同的運算元物件時,才會使用此方法,而這個 RiscV 模擬器並非如此。
接下來是述詞、來源和目的地的運算元列舉項目,用於識別必須建立的運算元。這些值來自 riscv32i_enums.h
中的三個 OpEnums,如下所示:
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,
};
請注意,如果回頭查看 riscv32.isa
檔案,您會注意到,這些名稱對應至每個指令宣告中所用的來源與目的地運算元名稱組合。針對代表不同位元欄位和運算子類型的運算子,使用不同的運算子名稱,即可更輕鬆地編寫編碼類別,因為列舉成員會明確決定要傳回的確切運算子類型,因此不必考慮槽、項目或 Opcode 參數的值。
最後,針對來源和目的地運算元,會傳入運算元的序號位置 (同樣地,我們可以忽略這項資訊),而針對目的地運算元,則會傳入在指令發出後,目的地結果可供後續指令使用的延遲時間 (以週期為單位)。在模擬器中,這個延遲時間為 0,表示指令會立即將結果寫入註冊。
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
如果特定目的地運算元在 .isa
檔案中已指定為 *
,則最終函式可用來取得該運算元的延遲時間。這很少見,且不會用於這個 RiscV 模擬器,因此我們實作此函式時只會傳回 0。
定義編碼類別
標頭檔案 (.h)
方法
開啟檔案 riscv32i_encoding.h
。所有必要的包含檔案都已新增,命名空間也已設定。所有程式碼新增作業已在註解 // Exercise 2.
後完成
首先,我們要定義從產生的介面繼承的類別 RiscV32IEncoding
。
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
接著,建構函式應採用狀態例項的指標,在本例中為 riscv::RiscVState
的指標。應使用預設解構函式。
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
加入所有介面方法之前,讓我們加入 RiscV32Decoder
呼叫的方法,剖析指示:
void ParseInstruction(uint32_t inst_word);
接下來,我們來新增那些有簡單覆寫值的方法,並刪除未使用的參數名稱:
// 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; }
最後,請加入公開介面的其他方法覆寫值,但將實作項目延後至 .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;
為了簡化每個運算元 getter 方法的實作,我們會建立兩個可呼叫 (函式物件) 陣列,分別以 SourceOpEnum
和 DestOpEnum
成員的數值建立索引。這樣一來,這些方法的內文就會縮減為呼叫函式物件,針對傳入的列舉值傳回其傳回值。
為了整理這兩個陣列的初始化作業,我們定義了兩個私人方法,如下所示,這些方法會從建構函式中呼叫:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
資料成員
必要的資料成員如下:
state_
來保留riscv::RiscVState *
值。uint32_t
類型的inst_word_
,包含目前指令字詞的值。opcode_
可保留由ParseInstruction
方法更新的目前指令運算碼。此類型具有OpcodeEnum
類型。source_op_getters_
陣列,用於儲存用來取得來源運算子物件的可呼叫項。陣列元素的類型為absl::AnyInvocable<SourceOperandInterface *>()>
。dest_op_getters_
陣列,用於儲存用來取得目的運算子物件的可呼叫項。陣列元素的類型為absl::AnyInvocable<DestinationOperandInterface *>()>
。xreg_alias
RiscV 整數註冊 ABI 名稱陣列,例如使用「zero」和「ra」而非「x0」和「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"};
如果您需要協助 (或想查看作品),請按這裡查看完整答案。
來源檔案 (.cc)。
開啟檔案 riscv32i_encoding.cc
。所有必要的包含檔案都已新增,命名空間也已設定。所有程式碼新增作業都會在註解 // Exercise 2.
後方執行
輔助函式
我們將從編寫幾個輔助函式開始,用於建立來源和目的地註冊運算元。系統會將這些屬性套用至暫存類型,並呼叫 RiscVState
物件取得註冊物件的控制代碼,然後在註冊物件中呼叫運算元工廠方法。
我們先從目的運算子輔助程式開始:
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);
}
如您所見,有兩個輔助函式。第二個方法會使用額外的參數 op_name
,讓運算元的名稱或字串表示法與基礎登錄不同。
來源運算元輔助程式類似:
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;
}
建構函式和介面函式
建構函式和介面函式非常簡單。建構函式只會呼叫兩個初始化方法,為運算子 getter 初始化可呼叫項陣列。
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
會儲存指令字,然後儲存從呼叫至二進位解碼器產生的程式碼中取得的 Opcode。
// 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_);
}
最後,運算子 getter 會根據使用目的地/來源運算子列舉值的陣列查詢,從呼叫的 getter 函式傳回值。
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)]();
}
陣列初始化方法
如您所料,大部分的工作都是在初始化 getter 陣列,但別擔心,這項作業是使用簡單的迴圈模式完成。由於只有幾個目的地運算元,因此我們先從 InitializeDestinationOpGetters()
開始。
從 riscv32i_enums.h
回呼產生的 DestOpEnum
類別:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
針對 dest_op_getters_
,我們需要初始化 4 個項目,分別用於 kNone
、kCsr
、kNextPc
和 kRd
。為方便起見,每個項目都會使用 lambda 進行初始化,但您也可以使用任何其他形式的可呼叫項目。lambda 的簽名為 void(int latency)
。
到目前為止,我們並未多談 MPACT-Sim 中定義的不同類型目的運算子。在本練習中,我們只會使用兩種類型:在 register.h
中定義的 generic::RegisterDestinationOperand
,以及在 devnull_operand.h
中定義的 generic::DevNullOperand
。這些運算元的詳細資料目前並不重要,但請注意,前者用於寫入暫存器,而後者會忽略所有寫入作業。
第一個 kNone
項目非常重要,只要傳回空值,並視需要記錄錯誤即可。
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
接下來是 kCsr
。我們要稍微作弊。「hello world」程式不依賴任何實際的 CSR 更新,但有執行 CSR 指令的程式碼樣板。解決方法是使用名為「CSR」的一般登錄器,並將所有這類寫入作業導向至該登錄器。
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
接著是 kNextPc
,它是指「pc」註冊。它用於所有分支和跳躍指令的目標。名稱在 RiscVState
中定義為 kPcName
。
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
最後是 kRd
目的地運算元。在 riscv32i.isa
中,運算元 rd
只用於參照指令字的 "rd" 欄位中編碼的整數註冊,因此不會造成參照的歧義。只有一個小工具。註冊 x0
(abi 名稱 zero
) 已硬連線至 0,因此我們會為該註冊使用 DevNullOperand
。
因此,在這個 getter 中,我們會先使用 .bin_fmt 檔案產生的 Extract
方法,擷取 rd
欄位中的值。如果值為 0,我們會傳回「DevNull」運算元,否則會傳回正確的暫存器運算元,並注意使用適當的暫存器別名做為運算元名稱。
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]);
)
}
}
現在在 InitializeSourceOperandGetters()
方法上,模式相同,但細節稍有不同。
首先,讓我們來看看第一個教學課程中從 riscv32i.isa
產生的 SourceOpEnum
:
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,
};
檢查成員,除了 kNone
,它們分為兩組。一個是立即運算元:kBimm12
、kImm12
、kJimm20
、kSimm12
、kUimm20
和 kUimm5
。其他是寄存器運算元:kCsr
、kRs1
和 kRs2
。
kNone
運算元會以與目的地運算元相同的方式處理,也就是傳回 nullptr。
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
接下來,我們來處理註冊運算元。我們會以類似於處理對應目的運算元的做法處理 kCsr
,只要使用「CSR」做為註冊名稱呼叫輔助函式即可。
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
運算元 kRs1
和 kRs2
的處理方式與 kRd
相同,但我們不想更新 x0
(或 zero
),因此想確保我們一律從該運算元讀取 0。為此,我們會使用 literal_operand.h
中定義的 generic::IntLiteralOperand<>
類別。這個運算元是用來儲存常值 (而非模擬的立即值)。否則模式相同:首先從指令字詞中擷取 rs1/rs2 值,如果是零,則傳回具有 0 範本參數的字面值運算元,否則使用輔助函式傳回一般註冊來源運算元,並使用 abi 別名做為運算元名稱。
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]);
};
最後,我們會處理不同的立即運算元。即時值會儲存在 immediate_operand.h
中定義的類別 generic::ImmediateOperand<>
例項中。立即運算元的不同 getter 之間唯一的差異,就是使用哪個 Extractor 函式,以及根據位元欄位,儲存類型是否帶正負號。
// 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_));
};
}
如需協助 (或想檢查自己的答案),請參閱這篇文章的完整解答。
本教學課程到此結束。希望對您有所幫助。