命令セマンティック関数のチュートリアル

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

  • セマンティック関数を使用して命令セマンティクスを実装する方法を学びます。
  • セマンティック関数と ISA デコーダの説明との関係について説明します。
  • RiscV RV32I 命令の命令セマンティック関数を記述します。
  • 小規模な「Hello World」を実行して最終的なシミュレータをテストする作成します。

セマンティック関数の概要

MPACT-Sim のセマンティック関数は、オペレーションを 命令の副作用がシミュレートされた状態で可視化されるようにする 同じように、命令の副作用は、 あります。デコードされた各命令のシミュレータの内部表現 セマンティック関数を呼び出すための呼び出し可能オブジェクトが含まれています。 できます。

セマンティック関数にはシグネチャ void(Instruction *)、つまり Instruction クラスのインスタンスへのポインタを受け取る関数と、 は void を返します。

Instruction クラスは、 instruction.h

セマンティック関数を記述する目的で特に注目すべき点は、 送信元と宛先のオペランド インターフェース ベクトルに Source(int i) メソッド呼び出しと Destination(int i) メソッド呼び出し。

送信元と宛先のオペランド インターフェースを以下に示します。

// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
 public:
  // Methods for accessing the nth value element.
  virtual bool AsBool(int index) = 0;
  virtual int8_t AsInt8(int index) = 0;
  virtual uint8_t AsUint8(int index) = 0;
  virtual int16_t AsInt16(int index) = 0;
  virtual uint16_t AsUint16(int) = 0;
  virtual int32_t AsInt32(int index) = 0;
  virtual uint32_t AsUint32(int index) = 0;
  virtual int64_t AsInt64(int index) = 0;
  virtual uint64_t AsUint64(int index) = 0;

  // Return a pointer to the object instance that implements the state in
  // question (or nullptr) if no such object "makes sense". This is used if
  // the object requires additional manipulation - such as a fifo that needs
  // to be pop'ed. If no such manipulation is required, nullptr should be
  // returned.
  virtual std::any GetObject() const = 0;

  // Return the shape of the operand (the number of elements in each dimension).
  // For instance {1} indicates a scalar quantity, whereas {128} indicates an
  // 128 element vector quantity.
  virtual std::vector<int> shape() const = 0;

  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;

  virtual ~SourceOperandInterface() = default;
};
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
 public:
  virtual ~DestinationOperandInterface() = default;
  // Allocates a data buffer with ownership, latency and delay line set up.
  virtual DataBuffer *AllocateDataBuffer() = 0;
  // Takes an existing data buffer, and initializes it for the destination
  // as if AllocateDataBuffer had been called.
  virtual void InitializeDataBuffer(DataBuffer *db) = 0;
  // Allocates and initializes data buffer as if AllocateDataBuffer had been
  // called, but also copies in the value from the current value of the
  // destination.
  virtual DataBuffer *CopyDataBuffer() = 0;
  // Returns the latency associated with the destination operand.
  virtual int latency() const = 0;
  // Return a pointer to the object instance that implmements the state in
  // question (or nullptr if no such object "makes sense").
  virtual std::any GetObject() const = 0;
  // Returns the order of the destination operand (size in each dimension).
  virtual std::vector<int> shape() const = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
};

正規の 3 オペランドのセマンティック関数を記述する基本的な方法 たとえば、32 ビットの add 命令の場合は次のようになります。

void MyAddFunction(Instruction *inst) {
  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);
  uint32_t c = a + b;
  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, c);
  db->Submit();
}

この関数の構成要素を詳しく見ていきましょう。最初の 2 行は、 ソースオペランド 0 と 1 から読み取ります。AsUint32(0) の呼び出し 基になるデータを uint32_t 配列として解釈し、0 番目の配列をフェッチします 要素です。これは、基になるレジスタまたは値が 配列の値があるかどうかを示します。ソースオペランドのサイズ(要素単位)は ソースオペランド メソッド shape() から取得され、ベクトルを返します。 各ディメンションの要素数が含まれています。このメソッドは {1} を返します。 はスカラー、{16} は 16 要素のベクトル、{4, 4} は 4x4 配列を表します。

  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);

次に、c という一時的な uint32_t に値 a + b が割り当てられます。

次の行については、もう少し説明が必要になる場合があります。

  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();

DataBuffer は参照カウント オブジェクトで、 シミュレートされた状態を表します。比較的型指定はありませんが、 割り当て元のオブジェクトに基づいてサイズを決定できます。この場合 サイズは sizeof(uint32_t)。このステートメントは、指定されたサイズの新しいデータ バッファを割り当てます。 このデスティネーション オペランドのターゲットとなるデスティネーション。 32 ビット整数レジスタ。また、DataBuffer は UDM 値で初期化され、 アーキテクチャのレイテンシが 大きく影響しますこれは、トレーニングの あります。

次の行は、データ バッファ インスタンスを uint32_t の配列として扱い、 c に格納されている値を 0 番目の要素に書き込みます。

  db->Set<uint32_t>(0, c);

最後に、最後のステートメントでデータ バッファをシミュレータに送信し、使用できるようにします。 ターゲット マシンの状態(この場合はレジスタ)の その命令がデコードされたときに設定された命令と 宛先オペランド ベクトルが入力されます。

これはかなり短い機能ですが、少量のボイラープレートがあります。 命令の後に命令を実装するときに反復的になるコードです。 また、命令の実際のセマンティクスがわかりにくくなる可能性があります。順序 ほとんどの命令のセマンティック関数の記述をさらに簡素化します。 テンプレート化されたヘルパー関数が多数定義されており、 instruction_helpers.h。 これらのヘルパーでは、1 つ、2 つ、または 3 つの命令のボイラープレート コードが非表示になります。 1 つのデスティネーションオペランドの 2 つに分けられますでは、Google Chat の オペランド ヘルパー関数:

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument1, Argument2)> operation) {
  Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
  Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument, Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Result, Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

次のようなステートメントを使用する代わりに、

  uint32_t a = inst->Source(0)->AsUint32(0);

ヘルパー関数では以下を使用します。

generic::GetInstructionSource<Argument>(instruction, 0);

GetInstructionSource は、テンプレート化されたヘルパー関数のファミリーです。 テンプレート化されたアクセス方法を指示ソースに提供するために使用されます。 オペランドになりますこれらがないと、各指示ヘルパー関数が ソース オペランドに正しい ID でアクセスするために、 As<int type>() 関数を使用します。これらのテンプレートの定義は 関数を instruction.h:

ご覧のとおり、3 つの実装があります。ソースとターゲットに オペランド タイプは、デスティネーションが同じでも、デスティネーションと すべて異なっているのかどうかなどを確認できます。各バージョンの この関数は、命令インスタンスへのポインタと呼び出し可能 (ラムダ関数を含む)。これにより、add を書き換えることができます。 上記のセマンティック関数を次のように作成します。

void MyAddFunction(Instruction *inst) {
  generic::BinaryOp<uint32_t>(inst,
                              [](uint32_t a, uint32_t b) { return a + b; });
}

ビルド内で bazel build -c optcopts = ["-O3"] を使用してコンパイルした場合 完全にインライン化され、オーバーヘッドは発生しないため、 パフォーマンスを損なうことはありません。

前述のように、単項、二項、三項のスカラーのヘルパー関数があります。 ベクトル表現が含まれています。また、有用なテンプレートとしても機能します。 をご覧ください。


初期ビルド

ディレクトリを riscv_semantic_functions に変更していない場合は、変更します。 います。次に、次のようにプロジェクトをビルドします。このビルドは成功するはずです。

$  bazel build :riscv32i
...<snip>...

ファイルは生成されないため、これは単なるドライランで 確認します


3 つのオペランド ALU 命令を足し算する

次に、一般的な 3 オペランド ALU 用のセマンティック関数を追加しましょう。 できます。rv32i_instructions.cc ファイルを開き、 不足している定義は、進めていくと rv32i_instructions.h ファイルに追加されます。

追加する手順は以下のとおりです。

  • add - 32 ビット整数の加算。
  • and - 32 ビットのビット演算 AND。
  • or - 32 ビットのビット演算 OR。
  • sll - 32 ビットの論理シフト左。
  • sltu - 32 ビット符号なしセットの小なり値。
  • sra - 32 ビット算術右シフト。
  • srl - 32 ビットの論理右シフト。
  • sub - 32 ビット整数の減算。
  • xor - 32 ビットのビット XOR。

これまでのチュートリアルを完了している場合、 レジスタ / レジスタ命令とレジスタ - 即時命令の間の 使用します。セマンティック関数に関しては、その必要がなくなりました。 オペランド インターフェースは、どのオペランドからでもオペランド値を読み取ります。 セマンティック関数は、登録または即時のどちらに 基になるソースオペランドが何であるかがわかります

sra を除き、上記の手順はすべて、オペレーションとして処理できます。 32 ビットの符号なし値。これらには BinaryOp テンプレート関数を使用できます。 先ほど見たテンプレート タイプ引数は 1 つのみでした。以下の欄に入力します。 rv32i_instructions.cc の関数本体を適宜変更します。なお、 シフト命令の第 2 オペランドのビットがシフト演算に使用される します。それ以外の場合、すべてのオペレーションは src0 op src1 の形式になります。

  • add: a + b
  • and: a & b
  • or: a | b
  • sll: a << (b & 0x1f)
  • sltu: (a < b) ? 1 : 0
  • srl: a >> (b & 0x1f)
  • sub: a - b
  • xor: a ^ b

sra には、3 つの引数 BinaryOp テンプレートを使用します。ここに示されている 最初の型引数は結果の型 uint32_t です。2 つ目は、 ソースオペランド 0 の型。この例では int32_t、最後のオペランドは型 ソースオペランド 1 の値です。この例では uint32_t です。これにより、sra の本文が作成されます。 セマンティック関数:

  generic::BinaryOp<uint32_t, int32_t, uint32_t>(
      instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });

変更を加えてビルドします。自分の作業内容を rv32i_instructions.cc


2 つのオペランド ALU 命令を加算する

2 つのオペランド ALU 命令は、luiauipc の 2 つだけです。前者 は、事前にシフトされたソース オペランドを宛先に直接コピーします。後者 命令アドレスを命令アドレスを命令アドレスの前に追加し、命令アドレスを あります。命令アドレスには address() メソッドからアクセスできます。 Instruction オブジェクトの

ソースオペランドが 1 つしかないため、代わりに BinaryOp は使用できません。 UnaryOp を使用する必要があります。ソースと特徴量の両方を デスティネーション オペランドを uint32_t として扱うには、単一の引数テンプレートを使用できます。 できます。

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

lui のセマンティック関数の本体はごくわずかです。 ソースを返すだけですauipc のセマンティック関数では、マイナー これは、Instructionaddress() メソッドにアクセスする必要があるためです。 作成します。答えは、instruction をラムダ キャプチャに追加して、 使用できます。前のように [](uint32_t a) { ... } ではなく、ラムダは [instruction](uint32_t a) { ... } と記述する必要があります。 これで、ラムダ本体で instruction を使用できるようになりました。

変更を加えてビルドします。自分の作業内容を rv32i_instructions.cc


制御フローの変更手順を追加する

実装する必要がある制御フローの変更手順は、 条件分岐命令(特定の条件下で行われる、より短い分岐) 比較が当てはまる)、ジャンプ&リンク命令は、 関数呼び出しを実装します(-and-link は、links 変数を設定することで ゼロに登録され、その書き込みが NoOps になります)。

条件分岐の手順を追加する

分岐命令にはヘルパー関数がないため、2 つのオプションがあります。 セマンティック関数をゼロから記述するか、ローカル ヘルパー関数を記述します。 6 つの分岐命令を実装する必要があるため、後者は 6 つの分岐命令を実装する 手間がかかります。その前にブランチの実装を見てみましょう。 命令セマンティック関数をゼロから作成しました。

void MyConditionalBranchGreaterEqual(Instruction *instruction) {
  int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
  int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
  if (a >= b) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0,m target);
    db->Submit();
  }
}

ブランチ命令によって異なるのは、ブランチ 2 つの条件、および 2 つのデータ型(符号付きと符号なし 32 ビット整数) ソースオペランド。つまり、トレーニング プロセスでの ソースオペランド。ヘルパー関数自体は、Instruction インスタンスと、bool を返す std::function などの呼び出し可能オブジェクト 渡します。ヘルパー関数は次のようになります。

template <typename OperandType>
static inline void BranchConditional(
    Instruction *instruction,
    std::function<bool(OperandType, OperandType)> cond) {
  OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
  OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
  if (cond(a, b)) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0, target);
    db->Submit();
  }
}

これで、セマンティック関数 bge(符号付き分岐以上)を記述できます。 例:

void RV32IBge(Instruction *instruction) {
  BranchConditional<int32_t>(instruction,
                             [](int32_t a, int32_t b) { return a >= b; });
}

残りのブランチ指示は次のとおりです。

  • Beq - 分岐が等しい。
  • Bgeu - 大きいか等しい分岐(符号なし)。
  • Blt - より小さい分岐(符号付き)。
  • Bltu - より小さい分岐(符号なし)。
  • Bne - 分岐が等しくありません。

これらのセマンティック関数を実装するための変更を行い、 再ビルドします。自分の作業内容を rv32i_instructions.cc

ジャンプとリンクのヘルパー関数を作成する意味はない 最初から作成する必要があります。まず、 指示セマンティクスに注目します。

jal 命令はソースオペランド 0 からオフセットを受け取り、 現在の pc(命令アドレス)からジャンプ ターゲットを計算できます。ジャンプ ターゲット 宛先オペランド 0 に書き込まれます。返品先住所は、 次のシーケンス命令です。この値は、現在の そのアドレスにマッピングされます。返品先住所は 宛先オペランド 1命令オブジェクト ポインタを 呼び出すことができます。

jalr 命令は、ベースレジスタをソースオペランド 0 として、またオフセットを受け取ります。 がソースオペランド 1 として実行され、それらが加算されてジャンプ ターゲットが計算されます。 それ以外の場合は、jal 命令と同じです。

命令セマンティクスの記述に基づいて、次の 2 つのセマンティックを 構築します。自分の作業内容を rv32i_instructions.cc


メモリストア命令を追加する

実装する必要があるストア命令は 3 つあります。ストアバイトです。 格納(sb)、ハーフワード(sh)、格納する単語(sw)です。店舗に関する手順 これまでに実装した手順とは異なっており、 ローカル プロセッサの状態に書き込みを行います。代わりに、システム リソースに書き込みます。 できます。MPACT-Sim ではメモリを命令オペランドとして扱いません。 そのため、別の方法でメモリアクセスを実行する必要があります。

MPACT-Sim の ArchState オブジェクトにメモリアクセス メソッドを追加します。 ArchState から派生した新しい RiscV 状態オブジェクトを作成します。 このページで追加できますArchState オブジェクトは、次のようなコアリソースを管理します。 レジスタやその他の状態オブジェクトです。また、トラフィックを送信するのに使用される遅延線も 宛先オペランドのデータ バッファをバッファリングし、 呼び出すことができます。ほとんどの指導は、知識がなくても実装できる ただし、メモリ オペレーションや、他の特定のシステム 命令は、この状態オブジェクトに存在する機能を必要とします。

次の fence 命令のセマンティック関数を見てみましょう。 すでに rv32i_instructions.cc に実装されている例を取り上げます。fence 特定のメモリ オペレーションが実行されてから、命令の 完了していません。命令間のメモリの順序付けを保証するために使用されます。 命令の実行前に実行されるものと、命令の後に実行されるものがあります。

// Fence.
void RV32IFence(Instruction *instruction) {
  uint32_t bits = instruction->Source(0)->AsUint32(0);
  int fm = (bits >> 8) & 0xf;
  int predecessor = (bits >> 4) & 0xf;
  int successor = bits & 0xf;
  auto *state = static_cast<RiscVState *>(instruction->state());
  state->Fence(instruction, fm, predecessor, successor);
}

fence 命令のセマンティック関数の重要な部分は、最後の 2 つ 作成します。まず、Instruction のメソッドを使用して状態オブジェクトを取得します。 クラスと downcast<> を RiscV 固有の派生クラスに追加します。次に、Fence メソッド(RiscVState クラスのメソッド)を呼び出して、フェンス オペレーションを実行します。

ストアの手順も同様に機能します。まず、IP アドレスの メモリアクセスは、ベース命令とオフセット命令のソースオペランドから計算されます。 格納される値は次のソースオペランドから取得されます。次に、 RiscV 状態オブジェクトは、state() メソッド呼び出しによって取得されます。 static_cast<> になり、適切なメソッドが呼び出されます。

RiscVState オブジェクトの StoreMemory メソッドは比較的シンプルですが、 注意が必要な影響は次のとおりです。

  void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);

ご覧のとおり、このメソッドは 3 つのパラメータを受け取ります。 命令自体、ストアのアドレス、DataBuffer へのポインタ インスタンスが作成されますサイズを指定する必要はありませんが、 DataBuffer インスタンス自体には size() メソッドが含まれます。ただし、 命令からアクセスできるデスティネーション オペランド。 適切なサイズの DataBuffer インスタンスを割り当てます。代わりに、次の操作をします。 次のように、db_factory() メソッドから取得した DataBuffer ファクトリを使用します。 Instruction インスタンス。ファクトリのメソッドは Allocate(int size) です。 必要なサイズの DataBuffer インスタンスを返します。例を示します。 これを使用して、ハーフワードストアに DataBuffer インスタンスを割り当てる方法の (auto は右手から型を推測する C++ の機能です あります)。

  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(uint16_t));

DataBuffer インスタンスを作成したら、通常どおり書き込むことができます。

  db->Set<uint16_t>(0, value);

次に、これをメモリストア インターフェースに渡します。

  state->StoreMemory(instruction, address, db);

まだ終わりではありません。DataBuffer インスタンスは参照カウントされます。この 通常は Submit メソッドで認識、処理されるため、 できるだけシンプルにすることです。ただし、StoreMemory は そのように記述します。動作中に DataBuffer インスタンスが IncRef されます。 完了したら DecRef します。ただし、セマンティック関数が DecRef 参照しているため、再利用されることはありません。したがって、最後の行には、 次のようになります。

  db->DecRef();

ストア関数は 3 つあります。異なるのは、 発生します。これは他の地域の ヘルパー関数を使用します。ストア関数で異なる点は そのため、テンプレートではそれを引数として指定する必要があります。 それ以外は、Instruction インスタンスのみを渡す必要があります。

template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->Set<ValueType>(0, value);
  state->StoreMemory(instruction, address, db);
  db->DecRef();
}

では、ストアのセマンティック関数を完成させてビルドします。お客様の 攻撃対象領域を rv32i_instructions.cc


メモリ読み込み命令を追加する

実装する必要がある読み込み手順は、次のとおりです。

  • lb - バイトを読み込み、単語に符号拡張します。
  • lbu - 符号なしバイトを読み込み、単語にゼロ拡張します。
  • lh - ハーフワードを読み込み、符号拡張を単語に拡張します。
  • lhu - ハーフワードを符号なし、ゼロ拡張でワードに読み込みます。
  • lw - 単語を読み込みます。

読み込み手順は、モデル化が必要な最も複雑な手順です。 説明します。店舗指示と類似しており、次のものを実行する必要があります。 RiscVState オブジェクトにアクセスしますが、読み込みのたびに複雑さが増します。 命令は 2 つのセマンティック関数に分割されます。1 つ目は 実効アドレスを計算するという点で、ストア命令と類似 メモリアクセスを開始します2 つ目の処理は、メモリがメモリまたは アクセスが完了し、メモリデータをレジスタの宛先に書き込みます。 オペランド。

まず、RiscVStateLoadMemory メソッド宣言を見てみましょう。

  void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
                  Instruction *child_inst, ReferenceCount *context);

StoreMemory メソッドとは異なり、LoadMemory は parameters: インスタンスへのポインタ Instruction 参照カウントの context オブジェクト。前者は子命令であり、 レジスタの書き戻しを実装します(ISA デコーダのチュートリアルで説明)。これは、 現在の Instruction インスタンスの child() メソッドを使用してアクセスする。 後者は から派生したクラスのインスタンスへのポインタで、 ReferenceCount。この場合は、DataBuffer インスタンスを保存します。 読み込まれたデータが含まれます。context オブジェクトは Instruction オブジェクトの context() メソッド(ほとんどの手順では nullptr に設定されます)。

RiscV のメモリ読み込みのコンテキスト オブジェクトは、次の構造体として定義されます。

// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
  explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
  ~LoadContext() override {
    if (value_db != nullptr) value_db->DecRef();
  }

  // Override the base class method so that the data buffer can be DecRef'ed
  // when the context object is recycled.
  void OnRefCountIsZero() override {
    if (value_db != nullptr) value_db->DecRef();
    value_db = nullptr;
    // Call the base class method.
    generic::ReferenceCount::OnRefCountIsZero();
  }
  // Data buffers for the value loaded from memory (byte, half, word, etc.).
  DataBuffer *value_db = nullptr;
};

読み込み命令は、データサイズ(バイト、 そして、読み込まれた値が符号拡張されているかどうかです。「 後者は命令にのみ考慮されます。まず、テンプレート化された ヘルパー関数を使用します。これは Chronicle の ただし、ソース オペランドにアクセスして値を取得することはしません。 コンテキスト オブジェクトが作成されます。

template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->set_latency(0);
  auto *context = new riscv::LoadContext(db);
  state->LoadMemory(instruction, address, db, instruction->child(), context);
  context->DecRef();
}

ご覧のとおり、主な違いは、割り当てられた DataBuffer インスタンス パラメータとして LoadMemory 呼び出しに渡されるだけでなく、 LoadContext オブジェクト。

命令のセマンティック関数はすべて非常によく似ています。まず、 LoadContext は、Instruction メソッド context() を呼び出して取得します。 LoadContext * に静的にキャストされます。第 2 に、(データに基づく) 読み込みデータの DataBuffer インスタンスから読み取られます。第三に、 DataBuffer インスタンスは、宛先オペランドから割り当てられます。最後に、 読み込まれた値が新しい DataBuffer インスタンスに書き込まれ、Submit されます。 ここでも、テンプレート化されたヘルパー関数を使うことをおすすめします。

template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
  auto *context = down_cast<riscv::LoadContext *>(instruction->context());
  uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, value);
  db->Submit();
}

では、最後のヘルパー関数とセマンティック関数を実装しましょう。お支払い 各ヘルパー関数のテンプレートで使用するデータ型に注意します。 その負荷のサイズ、符号付き/符号なしという特性に対応していることを できます。

自分の作業内容を rv32i_instructions.cc


最終的なシミュレータをビルドして実行する

難しい作業がすべて終わったので、最終的なシミュレータを作成しましょう。「 トップレベルの C++ ライブラリが提供されています。 other/ にあります。コードを詳しく調べる必要はありません。水 後の高度なチュートリアルでこのトピックにアクセスします。

作業ディレクトリを other/ に変更してビルドします。必要な依存関係は 表示されます。

$ cd ../other
$ bazel build :rv32i_sim

このディレクトリには、シンプルな「hello world」というファイル内で hello_rv32i.elf。このファイルでシミュレータを実行して結果を確認するには:

$ bazel run :rv32i_sim -- other/hello_rv32i.elf

次のような行が表示されます。

INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$

bazel run :rv32i_sim -- -i other/hello_rv32i.elf コマンドを使用して、シミュレータをインタラクティブ モードで実行することもできます。これにより、 使用できます。プロンプトに「help」と入力して、使用可能なコマンドを表示します。

$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000   addi           ra, 0, 0
[0] > help


    quit                           - exit command shell.
    core [N]                       - direct subsequent commands to core N
                                     (default: 0).
    run                            - run program from current pc until a
                                     breakpoint or exit. Wait until halted.
    run free                       - run program in background from current pc
                                     until breakpoint or exit.
    wait                           - wait for any free run to complete.
    step [N]                       - step [N] instructions (default: 1).
    halt                           - halt a running program.
    reg get NAME [FORMAT]          - get the value or register NAME.
    reg NAME [FORMAT]              - get the value of register NAME.
    reg set NAME VALUE             - set register NAME to VALUE.
    reg set NAME SYMBOL            - set register NAME to value of SYMBOL.
    mem get VALUE [FORMAT]         - get memory from location VALUE according to
                                     format. The format is a letter (o, d, u, x,
                                     or X) followed by width (8, 16, 32, 64).
                                     The default format is x32.
    mem get SYMBOL [FORMAT]        - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem SYMBOL [FORMAT]            - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem set VALUE [FORMAT] VALUE   - set memory at location VALUE(1) to VALUE(2)
                                     according to FORMAT. Default format is x32.
    mem set SYMBOL [FORMAT] VALUE  - set memory at location SYMBOL to VALUE
                                     according to FORMAT. Default format is x32.
    break set VALUE                - set breakpoint at address VALUE.
    break set SYMBOL               - set breakpoint at value of SYMBOL.
    break VALUE                    - set breakpoint at address VALUE.
    break SYMBOL                   - set breakpoint at value of SYMBOL.
    break clear VALUE              - clear breakpoint at address VALUE.
    break clear SYMBOL             - clear breakpoint at value of SYMBOL.
    break clear all                - remove all breakpoints.
    help                           - display this message.

_start:
80000000   addi           ra, 0, 0
[0] >

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