本教學課程的目標如下:
- 瞭解如何使用語意函式實作指令語意。
- 瞭解語意函式與 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}
,以及為 4x4 陣列傳回 {4, 4}
。
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
接著,系統會將 a + b
值指派給名為 c
的 uint32_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 中定義的許多範本式 helper 函式定義了這些函式。這些輔助程式會隱藏樣板程式碼,用於搭配一個、兩個或三個來源運算元和單一目的地運算元。我們來看看兩個運算元輔助函式:
// 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 opt
和 copts = ["-O3"]
進行編譯時,這項作業應可完全內嵌,且不會產生額外負擔,讓我們能夠簡潔地記錄,且不會影響效能。
如先前所述,也有一元、二元和三元純量指令和向量等效適用的輔助函式。這些範本也能做為實用的範本,讓您建立自己的輔助程式,以滿足無法適用於一般模型的指令。
初始版本
如果您尚未將目錄變更為 riscv_semantic_functions
,請立即進行這項操作。接著按照下列方式建構專案,這項建構作業應會成功。
$ bazel build :riscv32i
...<snip>...
系統不會產生任何檔案,因此這只是模擬測試,用來確認一切運作正常。
新增三個運算元件的 ALU 指令
接下來,我們將為一些通用的 3 運算元 ALU 指令新增語義函式。開啟檔案 rv32i_instructions.cc
,並確保任何缺少的定義會在我們繼續操作時新增至檔案 rv32i_instructions.h
。
我們會新增以下操作說明:
add
- 32 位元整數加法。and
- 32 位元位元運算的 and。or
- 32 位元位元或運算。sll
- 32 位元邏輯移位。sltu
- 32 位元未簽署的 set less-than。sra
:32 位元算術右移。srl
:32 位元邏輯右移。sub
- 32 位元整數減法。xor
- 32 位元位元 XOR。
如果您已完成之前的教學課程,可能會想起我們如何區分註冊操作說明和解碼器中的立即註冊操作說明。在語意函式中,我們不再需要執行這項操作。運算元介面會從運算元 (寄存器或立即值) 讀取運算元值,而語意函式完全不考慮基礎來源運算元的實際內容。
除了 sra
以外,上述所有指令都可以視為在 32 位元未簽署值上運作,因此對於這類指令,我們僅可透過單一範本類型引數使用先前探討的 BinaryOp
範本函式。請根據視實際情況填入 rv32i_instructions.cc
中的函式主體。請注意,只有第二個運算元到位移指示的 5 位元低位元會用於偏移量。否則,所有作業都會採用 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
,我們會使用三個引數 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 指令
只有兩個 2 運算元的 ALU 指令:lui
和 auipc
。前者會將預先移位的來源運算元直接複製到目的地。後者會在將指令位址寫入目的地之前,先將指令位址加到立即值。操作說明地址可透過 Instruction
物件的 address()
方法存取。
由於只有一個來源運算元,我們無法使用 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 檢查您的成果。
新增控制流程變更操作說明
您需要實作的控制流程變更指令分為條件式分支指令 (在比較結果為真時執行較短的分支) 和跳躍和連結指令,後者用於實作函式呼叫 (-and-link 會透過將連結註冊設為零來移除,讓這些寫入作業無效)。
新增條件式分支指示
沒有用於分支版本指令的輔助函式,因此有兩個選項。 從頭開始編寫語意函式,或編寫本機輔助函式。由於我們必須實作 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 位元 int)。這表示來源運算元需要一個範本參數。輔助函式本身需要取得 Instruction
執行個體和可呼叫的物件 (例如 std::function
),以參數形式傳回 bool
。輔助函式如下所示:
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 - 分支大於或等於 (未簽署)。
- Blt - 分支小於 (已簽署)。
- Bltu - 分支小於 (無符號)。
- Bne - 分支版本不相等。
請繼續進行變更,實作這些語意函式,然後重新建構。您可以依據 rv32i_instructions.cc 檢查成果。
新增跳躍和連結指令
為跳躍和連結指令編寫輔助函式並無意義,因此我們需要從頭開始編寫這些指令。我們先來看看操作說明語意
jal
指令會從來源運算元 0 取得偏移量,並將其加到目前的 PC (指令位址),以便計算跳躍目標。跳躍目標會寫入目的地運算元 0。傳回地址是下一個順序指令的地址。您可以將目前指令的大小加到其位址,藉此計算出該值。回傳位址會寫入至目的運算元 1。請記得在 lambda 擷取中加入指令物件指標。
jalr
指令會將基底登錄器視為來源運算元 0,並將偏移量視為來源運算元 1,然後將兩者加總,以便計算跳躍目標。否則,它與 jal
指令完全相同。
根據這些指令語意的說明,編寫兩個語意函式並建構。您可以將自己的成果與 rv32i_instructions.cc 進行比對。
新增記憶體儲存指令
我們需要實作三個商店操作說明:儲存位元組 (sb
)、儲存半字 (sh
) 和儲存文字 (sw
)。儲存操作說明不同於我們目前為止實作的操作說明,因此無法寫入本機處理器狀態。而是寫入系統資源 - 主記憶體。MPACT-Sim 不會將記憶體視為指令運算元,因此必須使用其他方法執行記憶體存取作業。
答案是將記憶體存取方法新增至 MPACT-Sim ArchState
物件,或者更準確地說,建立新的 RiscV 狀態物件,並從 ArchState
衍生,以便新增此物件。ArchState
物件會管理核心資源,例如暫存器和其他狀態物件。也會管理用於緩衝目的地運算元資料緩衝區的延遲行,直到這些緩衝區可以寫回註冊物件為止。您不必瞭解這個類別,也能實作大部分指令,但有些指令 (例如記憶體作業和其他特定系統指令) 需要在這個狀態物件中執行功能。
讓我們來看看已在 rv32i_instructions.cc
中實作的 fence
指令語意函式範例。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 專屬的衍生類別。接著,系統會呼叫 RiscVState
類別的 Fence
方法,執行圍欄操作。
商店販售方式也類似。首先,透過基本和偏移指令來源運算元計算記憶體存取的有效位址,接著系統會從下一個來源運算元擷取要儲存的值。接著,系統會透過 state()
方法呼叫和 static_cast<>
取得 RiscV 狀態物件,然後呼叫適當的方法。
RiscVState
物件 StoreMemory
方法相當簡單,但有幾個需要注意的影響:
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
如我們所見,這個方法會使用三個參數、商店指示本身的指標、商店地址,以及指向包含該商店資料的 DataBuffer
例項的指標。請注意,不需要指定大小,DataBuffer
例項本身包含 size()
方法。不過,沒有可供指示的目的地運算元存取,可用來分配適當大小的 DataBuffer
例項。我們需要使用從 Instruction
例項的 db_factory()
方法取得的 DataBuffer
工廠。該工廠具有 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();
儲存庫中有三個儲存庫函式,唯一的差別是記憶體存取大小。這聽起來是另一個本機範本輔助函式的絕佳機會。儲存函式唯一不同的部分是儲存值的類型,因此範本必須將該值設為引數。除了上述情況外,您只需要傳入 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
物件中。
子項指令語意函式都非常相似。首先,您可以透過呼叫 Instruction
方法 context()
取得 LoadContext
,然後將其靜態轉換為 LoadContext *
。其次,系統會從載入資料 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] >
本教學課程到此結束。希望對您有所幫助。