RiscV 統合デコーダ

このチュートリアルの目的は次のとおりです。

  • 生成された ISA とバイナリ デコーダがどのように連携するのかを学びます。
  • RiscV 用の完全な命令デコーダを作成するために必要な C++ コードを記述する 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;
};

ご覧のとおり、実装する必要があるメソッドは 1 つだけです。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++ コードを含む 4 つのソースファイルが表示されます。

  • riscv32i_decoder.h
  • riscv32i_decoder.cc
  • riscv32i_enums.h
  • riscv32i_enums.cc

最初のファイル riscv32i_decoder.h を開きます。3 つのクラスがあります。 次の点を確認する必要があります。

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

クラスの名前に注目してください。すべてのクラスには、名前が付けられている 「isa」で指定された名前のパスカルケース バージョン宣言します。 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_;
};

このクラスには仮想メソッドがないため、これはスタンドアロン クラスですが、 次の 2 点に注意します。まず、コンストラクタは引数として、 RiscV32IInstructionSetFactory クラス。このクラスは、Terraform で デコーダが使用して RiscV32Slot クラスのインスタンスを作成します。これは、 で定義されているように、slot RiscV32 に対して定義されたすべての命令をデコードします。 riscv32i.isa ファイル。次に、Decode メソッドは追加のパラメータを受け取り、 RiscV32IEncodingBase への型ポインタ。これはクラス 最初のチュートリアルで生成された isa デコーダとバイナリ間のインターフェース デコーダをコンパイルします。

RiscV32IInstructionSetFactory クラスは抽象クラスであり、この抽象クラスから 完全なデコーダのための独自の実装を導き出す必要があります。ほとんどの場合、この クラスは単純明快で、各オブジェクトのコンストラクタを呼び出すためのメソッドを提供するだけです。 スロットクラスを定義します。.isa ファイル今回の例では非常にシンプルです。 そのようなクラスが 1 つだけである: Riscv32Slot(名前 riscv32 のパスカルケース) Slot と連結されます)。すでに存在する場合は、メソッドは生成されません。 サブクラスを導出する際に有用性がある高度なユースケース コンストラクタを呼び出します。

このコースの後半では、最後のクラス RiscV32IEncodingBase について説明します。 別の演習で行います


トップレベルの命令デコーダを定義する

ファクトリ クラスを定義する

最初のチュートリアル用にプロジェクトを再構築した場合は、 riscv_full_decoder ディレクトリ。

riscv32_decoder.h ファイルを開きます。必要なすべてのインクルード ファイルには、 すでに追加され、名前空間が設定されています。

//Exercise 1 - step 1 とマークされたコメントの後に、クラスを定義します。 RiscV32IsaFactoryRiscV32IInstructionSetFactory から継承しています。

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

次に、CreateRiscv32Slot のオーバーライドを定義します。ここでは何も使用しないため、 Riscv32Slot の派生クラスを使用すると、 std::make_unique

std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
  return std::make_unique<Riscv32Slot>(state);
}

サポートが必要な場合(または作業内容を確認したい場合)は、 こちらをご覧ください。

デコーダクラスを定義する

コンストラクタ、デストラクタ、メソッド宣言

次に、デコーダクラスを定義します。上記と同じファイルで、 RiscV32Decoder の宣言。宣言をクラス定義に展開する ここで、RiscV32Decodergeneric::DecoderInterface を継承します。

class RiscV32Decoder : public generic::DecoderInterface {
  public:
};

次に、コンストラクタを記述する前に、コードを簡単に見てみましょう。 バイナリデコーダに関する 2 番目のチュートリアルで生成します。Chronicles SOAR プラットフォームの Extract 関数には、DecodeRiscVInst32 関数があります。

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

この関数はデコードする必要がある命令ワードを受け取り、 その命令に一致するオペコードが返されます。一方 RiscV32Decoder が実装する DecodeInterface クラスは、 あります。そのため、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 クラスのインスタンスを 他のデコーダ クラスのコンストラクタを使用します。適切な状態クラスは、 generic::ArchState から派生した riscv::RiscVState クラスに、 RiscV に対応しました。つまり、コンストラクタを宣言して、 次のように、statememory へのポインタを取得できます。

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

デフォルトのコンストラクタを削除し、デストラクタをオーバーライドします。

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

次に、オーバーライドする元の DecodeInstruction メソッドを宣言します。 generic::DecoderInterface

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

サポートが必要な場合(または作業内容を確認したい場合)は、 こちらをご覧ください。


データメンバーの定義

RiscV32Decoder クラスは、 ファクトリ クラスへのポインタが含まれます。

 private:
  riscv::RiscVState *state_;
  util::MemoryInterface *memory_;

また、Terraform から派生したエンコード クラスへのポインタも必要です。 RiscV32IEncodingBase です。ここでは RiscV32IEncoding とします( (演習 2 で行います)さらに、インスタンスへのポインタも必要です。 RiscV32IInstructionSet の場合は、次のように追加します。

  RiscV32IsaFactory *riscv_isa_factory_;
  RiscV32IEncoding *riscv_encoding_;
  RiscV32IInstructionSet *riscv_isa_;

最後に、メモリ インターフェースで使用するデータメンバーを定義する必要があります。

  generic::DataBuffer *inst_db_;

サポートが必要な場合(または作業内容を確認したい場合)は、 こちらをご覧ください。

Decoder クラスのメソッドを定義する

次に、コンストラクタ、デストラクタ、 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 メンバーを返します。 生成され、命令オペコードを識別します。OpcodeEnum クラスは次のとおりです。 (生成された ISA デコーダ ファイル riscv32i_enums.h に定義されています)。このメソッドは、 2 つのパラメータがありますが、ここではどちらも無視してかまいません。最初の スロットタイプ(riscv32i_enums.h でも定義される列挙型クラス)です。 RiscV にはスロットが 1 つしかないため、取り得る値は 1 つだけです。 SlotEnum::kRiscv32。2 つ目はスロットのインスタンス番号です( スロットに複数のインスタンスがあり、一部の 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);

次の 2 つの方法は、プロセッサ内のハードウェア リソースをモデル化するために使用します。 生理周期の精度を向上させるために使用しています。チュートリアルの演習では、 そのため、実装ではスタブ化され、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);

これら 3 つのメソッドは、オブジェクト内で使用されるオペランド オブジェクトへのポインタを返します。 任意の命令の値にアクセスする命令セマンティック関数 命令ソースの各オペランドに述語オペランドを実行し、新しい命令を 値を命令の宛先オペランドに渡す必要があります。RiscV はコンテナの このメソッドは nullptr を返すだけで済みます。

これらの関数でパラメータのパターンは類似しています。まず GetOpcode: スロットとエントリが渡されます。次に、変数のオペコードを オペランドを作成する必要がある命令です。これは、 異なるオペコードは同じオペランドに対して異なるオペランド オブジェクトを返す必要がある 異なるため、この RiscV シミュレータには関係ありません。

次は、Predicate、Source、Destination であるオペランド列挙エントリで、 作成されるオペランドを識別します。これらは 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 これらは、ソースと宛先のセットに対応します。 各命令の宣言で使用されるオペランド名別の Pod に 異なるビットフィールドとオペランドを表すオペランドのオペランド名 列挙型メンバーとして一意であるため、エンコード クラスを 返されるオペランド型は正確に決定します。 スロット、エントリ、またはオペコードのパラメータの値を考慮します。

最後に、送信元オペランドと宛先オペランドについては、 オペランドが渡されます(これも無視できます)。また、 オペランド、命令のオペレーションと実行との間で経過したレイテンシ(サイクル単位) この命令が発行され、宛先の結果が後続の命令で使用できるようになります。 このシミュレータでは、このレイテンシは 0 になります。つまり、この命令は すぐにレジスタに出力されます

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

最後の関数は、特定の宛先のレイテンシを取得するために使用されます。 オペランド(.isa ファイルで * として指定されている場合)。これは一般的ではありませんが、 この RiscV シミュレータでは使用されないため、この関数の実装では 0 を返します。


エンコード クラスを定義する

ヘッダー ファイル(.h)

メソッド

riscv32i_encoding.h ファイルを開きます。必要なすべてのインクルード ファイルには、 すでに追加され、名前空間が設定されています。コードの追加はすべて コメント // Exercise 2. に従って実行しました

まず、Terraform から継承するクラス 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;

各オペランド ゲッター メソッドの実装を簡素化するため callables(関数オブジェクト)の配列を 2 つ作成し、 それぞれ SourceOpEnum メンバーと DestOpEnum メンバーの数値。 このようにして、これらのメソッドの本体が削減されて、 渡されて戻り値を返す列挙値の関数オブジェクト あります。

これら 2 つの配列の初期化を整理するために、2 つの private 次のようにコンストラクタから呼び出されるメソッドです。

 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. に従って実行しました

ヘルパー関数

まず、Terraform の作成に使用するヘルパー関数をいくつか記述します。 ソースおよび宛先レジスタオペランド。これらは レジスタ型を取得し、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);
}

ご覧のように、2 つのヘルパー関数があります。2 つ目は、追加の 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;
}

コンストラクタ関数とインターフェース関数

コンストラクタとインターフェース関数は非常にシンプルです。コンストラクタ 2 つの初期化メソッドを呼び出すだけで、Google Cloud Storage の callables 配列を初期化できます。 オペランド ゲッター

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

最後に、オペランド ゲッターは、呼び出すゲッター関数から値を返します。 オペランドの列挙値を使用した配列ルックアップに基づいて計算されます。


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 単純な繰り返しパターンを使用して行われますが、では、 1 つしか存在しないため、最初は InitializeDestinationOpGetters() で始まります。 宛先オペランドが結合されます

riscv32i_enums.h から生成された DestOpEnum クラスを思い出してください。

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kNextPc = 2,
    kRd = 3,
    kPastMaxValue = 4,
  };

dest_op_getters_ では、4 つのエントリ(それぞれ kNone に 1 つずつ)を初期化する必要があります。 kCsrkNextPckRd。便宜上、各エントリは ただし、他の形式の呼び出し可能オブジェクトも使用できます。署名 void(int latency) です。

ここまでは、さまざまなタイプの目的地について詳しく説明してきませんでした。 MPACT-Sim で定義されたオペランドです。この演習では、使用するのは タイプ: generic::RegisterDestinationOperand が定義された register.h, および generic::DevNullOperanddevnull_operand.h。 これらのオペランドの詳細は 前者はレジスタへの書き込みに使用され、後者はすべての書き込みを無視します。

kNone の最初のエントリは簡単です。nullptr を返します。必要に応じて null を返します。 エラーが発生します。

void RiscV32IEncoding::InitializeDestinationOperandGetters() {
  // Destination operand getters.
  dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
    return nullptr;
  };

次は kCsr です。ここでは、ズルについて説明します。「Hello World」プログラム 実際の CSR アップデートには依存していませんが、 CSR 命令を実行します。解決策は、Cloud Shell でダミーする方法です。 「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」でエンコードされた整数レジスタを参照するためにのみ使用されます。フィールド だから、それが指す意味があいまいになることはありません。そこで、 ウォッチフェイスの追加機能は 1 つだけですレジスタ x0(Abi 名 zero)は 0 にハードワイヤーされています。 そのため、そのレジスタには DevNullOperand を使用します。

そのため、このゲッターでは、まず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 に加えて 2 つのグループに分類されます。1 本 即値オペランド: kBimm12kImm12kJimm20kSimm12kUimm20、 および kUimm5。もう 1 つはレジスタ オペランド(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 値。ゼロの場合、リテラルを返す テンプレート パラメータ 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]);
  };

最後に、異なる即値オペランドを扱います。即時の値は クラス generic::ImmediateOperand<> のインスタンスに格納される immediate_operand.h。 即値オペランドの異なるゲッターの違い 使用される 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_));
  };
}

サポートが必要な場合(または作業内容を確認したい場合)は、 こちらをご覧ください。

これでこのチュートリアルは終了です。以上、お役に立てば幸いです。