RiscV 整合式解碼器

本教學課程的目標是:

  • 瞭解產生的 ISA 和二進位解碼器如何相輔相成。
  • 編寫必要的 C++ 程式碼,建立 RiscV 的完整指示解碼器 結合 ISA 和二進位解碼器的 RV32I。

瞭解指令解碼器

指示解碼器會提供指示地址 記憶體中的指示字詞,並傳回完全初始化的 代表該指示的 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);

接著,我們來看看產生的程式碼需要的功能,以及產生的程式碼需要哪些資訊。

首先,請考慮檔案中的頂層類別 RiscV32IInstructionSet riscv32i_decoder.h,這是在本課程結束時產生的 ISA 解碼器。如要查看新內容,請前往 您會學到許多

$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...

現在將目錄改回存放區根目錄,接著來看看 所產生的來源為此,請將目錄變更為 bazel-out/k8-fastbuild/bin/riscv_isa_decoder (假設您使用 x86 格式) host - 如果是其他主機,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 類別的例項, 按照slot RiscV32 riscv32i.isa 檔案。第二,Decode 方法需要額外的參數 屬於 RiscV32IEncodingBase 類型的指標,這個類別會提供 第一個教學課程中產生的 isa 解碼器介面 編碼器-解碼器模型

RiscV32IInstructionSetFactory 類別是我們用來 必須自行實作完整的解碼器在大多數情況下 類別是精簡的:您只需要提供一個方法,為每個 .isa 檔案中定義的版位類別。在這個範例中,步驟非常簡單 只是單一類別:Riscv32Slot (名稱 riscv32 的純量案例) 以 Slot 串連)。系統不會為您產生這個方法 可能適用於衍生子類別的常見用途 並改為呼叫其建構函式。

我們稍後會介紹最終類別 RiscV32IEncodingBase 教學課程,因為這是另一個練習的主題


定義頂層指示解碼器

定義工廠類別

如果您已在第一個教學課程中重新建構專案,請務必改回使用 riscv_full_decoder 目錄。

開啟檔案 riscv32_decoder.h。所有必要 include 檔案中 ,且已設定命名空間。

在標示為 //Exercise 1 - step 1 的註解之後,定義類別 RiscV32IsaFactory 繼承自 RiscV32IInstructionSetFactory

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

這個函式會使用需要解碼的指令字詞,然後將其傳回 符合指示的運算程式碼另一方面, RiscV32Decoder 實作的 DecodeInterface 類別只會傳入 讓我們看看 DNS 解析 進一步探索內部和外部位址因此,RiscV32Decoder 類別必須能存取記憶體來 讀取要傳遞至 DecodeRiscVInst32() 的指示字詞。這項專案 存取記憶體的方式,就是透過 名為 util::MemoryInterface.../mpact/sim/util/memory,如下所示:

  // 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 的功能也就是說,我們必須宣告建構函式 可以指向 statememory

RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);

刪除預設建構函式並覆寫解構函式:

RiscV32Decoder() = delete;
~RiscV32Decoder() override;

接著,請宣告需要覆寫的 DecodeInstruction 方法 generic::DecoderInterface

generic::Instruction *DecodeInstruction(uint64_t address) override;

如果你需要協助 (或想查看作業),完整答案為 請按這裡


資料成員定義

RiscV32Decoder 類別需要私人資料成員來儲存 建構函式參數和 Factory 類別的指標。

 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 解碼器,傳入地址和編碼類別。

  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 成員 指令並識別指示運算程式碼OpcodeEnum 類別為 在產生的 isa 解碼器檔案 riscv32i_enums.h 中定義的這個方法 第一個 這些是版位類型 (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 位置和項目會傳入。接著是針對 必須建立運算元的指示。只有在廣告代碼的 不同運算碼需要針對相同運算元傳回不同的運算元物件 而這不是 RescV 模擬工具的例子

接著是述詞、來源和目的地 運算元列舉項目 識別要建立的運算元這些是 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敬上 您會注意到,這些內容會與來源和目的地的組合 每個指示的宣告中使用的運算元名稱。使用 代表不同 bitfields 和運算元的運算元名稱 因此能更輕鬆地編寫編碼類別,因為列舉成員獨有的 決定要傳回的確切運算元類型,但不需要 請考量運算單元、項目或 opcode 參數的值

最後,對於來源和目的地運算元來說, 運算元會傳入 (同樣可以忽略此),而對於目的地 運算元,在指示時間之間相隔的延遲時間 (以週期為單位) 發出,後續指令將提供目的地結果。 模擬工具中的延遲時間為 0,表示指令會寫入 並立即傳回結果

int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no);

最後一個函式則用於取得特定目的地的延遲時間 運算元 (如果 .isa 檔案中已指定為 *)。這並不常見 不會用於這個 RiscV 模擬工具 只會傳回 0


定義編碼類別

標頭檔案 (.h)

方法

開啟檔案 riscv32i_encoding.h。所有必要 include 檔案中 ,且已設定命名空間。所有新增的程式碼皆為 已追蹤註解 // 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 方法的實作。 我們會建立兩個 callable (函式物件) 陣列,由 SourceOpEnumDestOpEnum 成員的數值。 如此一來,這些主體會縮減為呼叫 所傳入列舉值的函式物件,並傳回其傳回結果 值。

為整理這兩個陣列的初始化作業,我們定義了兩個不公開的 建構函式呼叫的方法如下:

 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 名稱,例如「零」和 「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。所有必要 include 檔案中 ,且已設定命名空間。所有新增的程式碼皆為 已追蹤註解 // 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 &reg_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 &reg_name,
                                                   const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand(op_name);
  return op;
}

建構函式與介面函式

建構函式和介面函式非常簡單。建構函式 您只需呼叫兩個初始化方法,即可初始化 callables 陣列 運算元 getter

RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
  InitializeSourceOperandGetters();
  InitializeDestinationOperandGetters();
}

ParseInstruction 會儲存指令字詞,然後儲存其運算程式碼 值。

// 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 個項目,每個項目各一個 kNonekCsrkNextPckRd。為了方便起見,每個項目都會使用 lambda,但您也可以使用任何其他形式的呼叫。簽章 lambda 的結尾是 void(int latency)

到目前為止,我們還沒深入探討不同種類的目的地 以及 MPACT-Sim 中定義的運算元。針對本練習,我們只會使用兩個 類型:定義為 generic::RegisterDestinationOperand register.h, 和 generic::DevNullOperand 定義於 devnull_operand.h。 這些運算元的細節目前並不重要,除了 前者是用於寫入暫存器,後者則忽略所有寫入作業。

第一個 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,代表「電腦」註冊。做為目標 及跳轉指示名稱在 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 (Aabi 名稱 zero) 的有線編碼為 0, 因此,我們使用 DevNullOperand 來註冊該註冊。

因此在這個 getter 中,我們會先使用rd 從 .bin_fmt 檔案產生的 Extract 方法。如果值為 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() 方法, 兩者相似,但細節稍有不同

首先來看看產生的 SourceOpEnum 第一個教學課程中的 riscv32i.isa

  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 時,他們也會分為兩組。一 為立即運算元:kBimm12kImm12kJimm20kSimm12kUimm20、 和 kUimm5。另一個是註冊運算元:kCsrkRs1kRs2

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

運算元 kRs1kRs2 的處理方式與 kRd 相同,除非是 我們不想更新 x0 (或 zero),但我們想確保 我們一律從該運算元讀取 0為此,我們會使用 已定義 generic::IntLiteralOperand<> 類別 literal_operand.h。 這個運算元是用來儲存常值 (而非模擬的) 即時價值)。其他模式也相同:請先擷取 指示字詞的 rs1/rs2 值,如果為零,則會傳回常值 帶有範本參數的運算元,否則會傳回一般暫存器 使用輔助函式的來源運算元,以 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]);
  };

最後,我們會處理不同的立即運算元。即時值為 儲存在名為 generic::ImmediateOperand<> 類別的執行個體 immediate_operand.h。 不同 getter 之間直接運算元的唯一差異 是使用擷取器函式,而儲存體類型 不會加上符號。

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

如果你需要協助 (或想查看作業),完整答案為 請按這裡

本教學課程到此結束。希望對您有所幫助。