指令語意函式教學課程

本教學課程的目標是:

  • 瞭解如何使用語意函式實作指令語意。
  • 瞭解語意函式與 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();
}

現在來詳細說明此函式的各個部分。 函式主體讀取來源運算元 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);

接著,系統會將 a + b 值指派給名為 cuint32_t 臨時暫存檔。

下一行可能需要多方解釋:

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

DataBuffer 是參照的計數物件,用來將值儲存在 例如暫存器等模擬狀態這個類型相對不類型 目標大小在本例中,該大小是 sizeof(uint32_t)。這個陳述式會為 做為此目的地運算元的目標目的地,在本例中為 32 位元整數暫存器。DataBuffer 也會使用 架構延遲時間這項資訊會在操作說明指定時指定。 解碼器。

下一行會將資料緩衝區例項視為 uint32_t 的陣列, 會將儲存在 c 中的值寫入第 0 個元素。

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

最後,最後一個陳述式會將資料緩衝區提交至模擬工具 做為目標機器狀態的新值 (在此例中為暫存器) 指令解碼時設定的延遲時間,以及 已填入目的地運算元向量。

雖然這個功能相當簡單,但確實會有一些樣板 程式碼在指示後實作時可能會重複。 此外,這類指令可能會遮蓋指令的實際語意。在訂單中 進一步簡化為大多數指示編寫語意函式 您可以在 instruction_helpers.h。 這些輔助程式會隱藏樣板程式碼,用於指示一、二或三個 來源運算元和單一目的地運算元。以下分別介紹 運算元輔助函式:

// 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 是一系列範本式輔助函式, 用於提供範本存取方法給指示來源 運算元。如果沒有每個指令輔助函式 各類型都專門用於存取含有正確的程式碼運算元 As<int type>() 函式。您可以查看這些範本的定義 函式 instruction.h

如您所見,根據來源 運算元類型與目的地相同,無論目的地為 或來源是否不同每個 這個函式會指向指令執行個體,以及可呼叫的 (包括 lambda 函式)。這表示我們現在可以重新編寫 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>...

由於系統不會產生任何檔案,因此這只是 確定一切正常


新增三個運算元 ALU 指令

現在我們要針對一般 3 運算元 ALU 新增語意函式 操作說明。開啟「rv32i_instructions.cc」檔案,並確認是否有任何 缺少定義就會加進檔案 rv32i_instructions.h

新增的操作說明如下:

  • add - 32 位元整數加上。
  • and:32 位元位元和。
  • or:32 位元位元或。
  • sll - 32 位元邏輯移位。
  • sltu:32 位元無正負號集合小於。
  • sra:32 位元算術右移。
  • srl:32 位元邏輯右移。
  • sub - 減 32 位元整數。
  • xor - 32 位元位元 xor。

如果您已完成先前的教學課程,可能會還記得 註冊方式與立即註冊操作說明 傳給解碼器我們不再需要實作語意函式, 運算元介面會從運算元讀取運算元值 註冊或立即註冊時,您可以使用完全不受 瞭解基礎來源運算元的真正目的

除了 sra 以外,上述所有指令都可視為 32 位元無正負號值,因此可以使用 BinaryOp 範本函式 僅使用單一範本類型引數填入 rv32i_instructions.cc 中的函式主體。請注意 第二個運算元的位元的值用於位移 金額。否則所有作業會採用 src0 op src1 形式:

  • adda + b
  • anda & b
  • ora | b
  • slla << (b & 0x1f)
  • sltu(a < b) ? 1 : 0
  • srla >> (b & 0x1f)
  • suba - b
  • xora ^ b

如果是 sra,我們會使用三個引數 BinaryOp 範本。查看 範本,第一個類型引數是結果類型 uint32_t。第二項是 來源運算元 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


新增兩個運算元 ALU 指令

只有兩個運算元 ALU 指令:luiauipc。前 會將預先轉移的來源運算元直接複製到目的地。後者 會先將指示地址加到 ,然後再寫入 目的地。操作說明地址可透過 address() 方法存取 Instruction 物件的另一個節點。

由於只有一個來源運算元,我們無法使用 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 的語意函式導入了次要函式 問題,因為您需要存取 Instruction 中的 address() 方法 執行個體。答案是將 instruction 新增至 lambda 擷取, 可用於 lambda 函式主體。應寫入 [instruction](uint32_t a) { ... },而不是像之前的 [](uint32_t a) { ... }。 現在 instruction 可在 lambda 主體中使用。

請繼續進行變更並建構。您可以在 rv32i_instructions.cc


新增控制流程變更操作說明

您需要導入的控制流程變更操作說明分為 寫入條件分支版本操作說明 (如果遇到下列情況,則會產生更短的分支版本 這裡顯示的結果是正確的) 以及「跳轉連結」的操作說明 實作函式呼叫 (藉由設定連結 註冊零時差,讓這類寫入作業自動化。

新增條件分支版本操作說明

沒有用於分支版本指示的輔助函式,因此有兩個選項。 從頭開始編寫語意函式,或編寫本機輔助函式。 由於我們要實作 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();
  }
}

每個分支版本指示中唯一的差異是分支版本 限制條件,以及兩者中已簽署與未簽署的 32 位元整數 來源運算元。也就是說,我們需要擁有 來源運算元。輔助函式本身需要擷取 Instruction 執行個體和可呼叫物件,例如傳回 boolstd::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; });
}

其餘分支版本的操作說明如下:

  • 貝克 - 分支等於。
  • Bgeu - 分支版本大於或等於 (無符號)。
  • 組合 - 小於 (帶正負號) 的分支。
  • Bltu - 小於 (未簽署的) 分支版本。
  • Bne - 分支不等於。

繼續變更以實作這些語意函式 重新建構您可以在 rv32i_instructions.cc

撰寫跳轉和連結輔助函式時,並沒有明確的意義 指示,因此我們需要從頭開始撰寫這些項目。首先從 並檢查其指令語義

jal 指令會採用來源運算元 0 的偏移值,並將其新增至 目前的電腦 (指示位址) 用來計算跳躍目標。跳躍目標 寫入目的地運算元 0。傳回地址是 就會進入下一個序列中的操作說明方法是將目前 指引它的大小系統會將退貨地址寫入 目的地運算元 1。請記得將操作說明物件指標加入 lambda 擷取。

jalr 指令會將基礎暫存器當做來源運算元 0 和偏移量 做為來源運算元 1,然後相加,即可計算跳躍目標。 否則,會與 jal 指令相同。

根據這些操作說明語意,撰寫兩項語意 和建構您可以在 rv32i_instructions.cc


新增記憶體存放區操作說明

需要實作以下三種商店說明:商店位元組 (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 指令語意函式的關鍵部分是最後兩個函式 即可首先,系統會使用 Instruction 中的方法擷取狀態物件 類別,以及 downcast<> 至 RiscV 專用衍生類別然後Fence 系統會呼叫 RiscVState 類別的方法來執行圍欄作業。

商店販售方式也類似。首先是 系統會從基礎和偏移指令來源運算元計算記憶體存取權 則要儲存的值會從下一個來源運算元擷取。接下來, 您可以透過 state() 方法呼叫取得 RiscV 狀態物件 static_cast<>,並呼叫適當的方法。

RiscVState 物件 StoreMemory 方法相對簡單,但具有 請留意下列幾項影響:

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

如畫面所示,這個方法會使用三個參數,指向商店的指標 指示本身、商店地址,以及指向 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 並不是 。運作期間,系統會 IncRef DataBuffer 執行個體 但完成後為 DecRef。不過,如果語意函式 DecRef 本身的參照,永遠無法收回。因此,最後一行程式碼 :

  db->DecRef();

儲存庫有三個商店函式 唯一的差別是 讀取及寫入記憶體似乎是當地其他地區的大好良機 範本輔助函式商店功能的唯一差異是 商店值的類型,因此範本必須將該值做為引數。 除此之外,只需傳入 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 物件,但會在每次載入作業時增加複雜度 指令分成兩個獨立的語意函式。第一種是 和分店指示類似 可以計算有效的地址 並啟動記憶體存取第二項是在記憶體中 存取完畢,並將記憶體資料寫入註冊目的地 運算元。

首先,請查看 RiscVState 中的 LoadMemory 方法宣告:

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

相較於 StoreMemory 方法,LoadMemory 會使用兩個額外 參數:指向 Instruction 執行個體的指標,以及指向 參照計算的 context 物件。前者是子項指示, 實作註冊寫回寫入功能 (如 ISA 解碼器教學課程中所述)。這項服務 如要存取,請使用目前 Instruction 執行個體中的 child() 方法。 後者是指標的例項,該例項衍生自 在此情況下,ReferenceCount 會儲存 DataBuffer 例項, 包含載入的資料結構定義物件可透過 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;
};

除了資料大小 (位元組 字元集和字詞) 以及載入的值是否延伸。 後者只會將影響因素為 child 指令。建立一個範本 用於主負載指示的輔助函式。這個程序與 儲存指示,但無法存取來源運算元來取得值。 會建立情境物件

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 物件。

child 指令語意函式非常類似。首先, 呼叫 Instruction 方法 context() 即可取得 LoadContext。 靜態轉換為 LoadContext *其次,價值 (根據 type) 從載入資料 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] >

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