วัตถุประสงค์ของบทแนะนำนี้ได้แก่
- ดูวิธีใช้ฟังก์ชันเชิงความหมายเพื่อใช้ความหมายของคำสั่ง
- เรียนรู้ว่าฟังก์ชันความหมายเกี่ยวข้องกับคำอธิบายตัวถอดรหัส ISA อย่างไร
- เขียนฟังก์ชันอรรถศาสตร์คำสั่งสำหรับคำสั่ง RiscV RV32I
- ทดสอบเครื่องจำลองขั้นสุดท้ายโดยเรียกใช้ไฟล์ "Hello World" ขนาดเล็กที่เรียกใช้งานได้
ภาพรวมของฟังก์ชันความหมาย
ฟังก์ชันเชิงความหมายใน MPACT-Sim คือฟังก์ชันที่ใช้การดำเนินการของคำสั่งเพื่อให้เห็นผลข้างเคียงของคำสั่งในสถานะที่จำลอง เช่นเดียวกับที่จะเห็นผลข้างเคียงของคำสั่งเมื่อดำเนินการในฮาร์ดแวร์ การแสดงภายในของเครื่องมือจำลองของคำสั่งที่ถอดรหัสแต่ละรายการจะมี Callable ที่ใช้เรียกฟังก์ชันความหมายสำหรับคำสั่งนั้น
ฟังก์ชันเชิงความหมายมีลายเซ็น 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 แบบปกติ เช่น คำสั่ง add
แบบ 32 บิตมีดังนี้
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);
จากนั้นระบบจะกำหนด uint32_t
ชั่วคราวที่มีชื่อว่า c
ให้กับค่า a + b
บรรทัดถัดไปอาจต้องมีคำอธิบายเพิ่มเติมเล็กน้อย
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
DataBuffer ออบเจ็กต์ที่มีการนับการอ้างอิงซึ่งใช้เพื่อจัดเก็บค่าในสถานะที่จำลอง เช่น รีจิสเตอร์ ข้อมูลนี้ไม่มีการจัดประเภท แต่มีขนาดตามออบเจ็กต์ที่มาจากการจัดสรร ในกรณีนี้ ขนาดดังกล่าวคือ sizeof(uint32_t)
คำสั่งนี้จะจัดสรรบัฟเฟอร์ข้อมูลใหม่ที่มีขนาดใหม่สำหรับปลายทางที่เป็นเป้าหมายของตัวถูกดำเนินการปลายทางนี้ ซึ่งในกรณีนี้คือการลงทะเบียนจำนวนเต็ม 32 บิต นอกจากนี้ ระบบจะเริ่มต้น DataBuffer ด้วยเวลาในการตอบสนองเชิงสถาปัตยกรรมสําหรับคําสั่งด้วย โดยระบุระหว่างการถอดรหัสวิธีการ
บรรทัดถัดไปจะถือว่าอินสแตนซ์บัฟเฟอร์ข้อมูลเป็นอาร์เรย์ของ uint32_t
และเขียนค่าที่เก็บไว้ใน c
ไปยังองค์ประกอบที่ 0
db->Set<uint32_t>(0, c);
สุดท้าย คำสั่งสุดท้ายจะส่งบัฟเฟอร์ข้อมูลไปยังเครื่องจำลองเพื่อใช้เป็นค่าใหม่ของสถานะของเครื่องเป้าหมาย (ในกรณีนี้คือรีจิสเตอร์) หลังจากเวลาในการตอบสนองของคำสั่งซึ่งตั้งค่าไว้เมื่อถอดรหัสคำสั่งและแสดงเวกเตอร์ตัวถูกดำเนินการปลายทางแล้ว
แม้ว่าฟังก์ชันนี้จะสั้นพอสมควร แต่ก็ยังมีโค้ดที่ซ้ำกันอยู่บ้างเมื่อใช้คำสั่งทีละรายการ นอกจากนี้ ยังอาจทำให้ความหมายจริงของคำสั่งนั้นคลุมเครือ เพื่อให้การเขียนฟังก์ชันความหมายสำหรับคำสั่งส่วนใหญ่ง่ายขึ้น มีฟังก์ชัน helper ที่มีเทมเพลตหลายรายการที่กำหนดไว้ใน instruction_helpers.h ผู้ช่วยเหล่านี้จะซ่อนโค้ดต้นแบบสำหรับวิธีการที่มีตัวถูกดำเนินการต้นทาง 1 2 หรือ 3 รายการและตัวถูกดำเนินการปลายทางเดียว ลองมาดูฟังก์ชันตัวช่วยเหลือ ตัวดำเนินการ 2 ตัวต่อไปนี้
// 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
ดังที่คุณเห็น การใช้งานมี 3 แบบ โดยขึ้นอยู่กับว่าประเภทโอเปอเรเตอร์ของแหล่งที่มาเหมือนกับปลายทางหรือไม่ ปลายทางแตกต่างจากแหล่งที่มาหรือไม่ หรือทั้ง 2 อย่างแตกต่างกันหรือไม่ ฟังก์ชันแต่ละเวอร์ชันใช้พอยน์เตอร์ไปยังอินสแตนซ์คำสั่ง รวมถึงฟังก์ชันที่เรียกใช้ได้ (ซึ่งรวมถึงฟังก์ชัน 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 แบบ 3 ออบเจ็กต์ทั่วไปกัน เปิดไฟล์ rv32i_instructions.cc
และตรวจสอบว่ามีการใส่คำจำกัดความที่ขาดหายไปลงในไฟล์ rv32i_instructions.h
ขณะที่เราดำเนินการ
วิธีการที่เราจะเพิ่มมีดังต่อไปนี้
add
- การเพิ่มจำนวนเต็ม 32 บิตand
- บิตไวส์ 32 บิตและor
- การดำเนินการ OR แบบบิต 32 บิตsll
- เลื่อนไปทางซ้ายแบบตรรกะ 32 บิตsltu
- 32 บิตแบบไม่ลงนามที่มีค่าน้อยกว่าsra
- เลื่อนไปทางขวาแบบเลขคณิต 32 บิตsrl
- เลื่อนไปทางขวาแบบตรรกะ 32 บิตsub
- ลบจำนวนเต็ม 32 บิตxor
- การดำเนินการ XOR แบบบิต 32 บิต
หากคุณเคยทำบทแนะนำก่อนหน้านี้ คุณอาจจำได้ว่าเราได้แยกความแตกต่างระหว่างคำสั่งแบบรีจิสเตอร์กับรีจิสเตอร์กับคำสั่งแบบทันทีในโปรแกรมถอดรหัส เราไม่จำเป็นต้องทำแบบนั้นแล้วในเรื่องของฟังก์ชันทางอรรถศาสตร์ อินเทอร์เฟซตัวถูกดำเนินการจะอ่านค่าตัวถูกดำเนินการ ขึ้นอยู่กับว่าตัวถูกดำเนินการใดเป็นตัวถูกดำเนินการ ลงทะเบียน หรือทันที โดยฟังก์ชันเชิงความหมายไม่จำเป็นต้องเข้าใจตรงกันเลยว่าตัวถูกดำเนินการต้นฉบับคืออะไร
ยกเว้น sra
วิธีการทั้งหมดข้างต้นจะถือเป็นการดำเนินการสำหรับค่าที่ไม่มีเครื่องหมาย 32 บิต ดังนั้นในกรณีเหล่านี้ เราจึงใช้ฟังก์ชันเทมเพลต BinaryOp
ที่เราดูก่อนหน้านี้ด้วยอาร์กิวเมนต์ประเภทเทมเพลตเดียวเท่านั้น เติมเนื้อหาของฟังก์ชันใน rv32i_instructions.cc
ให้สอดคล้องกัน โปรดทราบว่าระบบจะใช้เฉพาะ 5 บิตล่างของออพเพอร์แรนด์ที่ 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
เราจะใช้เทมเพลต BinaryOp
ที่มีอาร์กิวเมนต์ 3 รายการ เมื่อดูที่เทมเพลต อาร์กิวเมนต์ประเภทแรกจะเป็นประเภทผลการค้นหา 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
เพิ่มคำสั่ง ALU ที่มี 2 ออบเจ็กต์
คำสั่ง ALU ที่มี 2 ออบเจ็กต์มีเพียง 2 คำสั่ง ได้แก่ lui
และ auipc
คำสั่งแรกจะคัดลอกโอเปอเรนด์ต้นทางที่เลื่อนไว้ล่วงหน้าไปยังปลายทางโดยตรง แบบหลังจะเพิ่มที่อยู่สำหรับคำสั่งต่อท้ายก่อนที่จะเขียนในปลายทาง คุณเข้าถึงที่อยู่คำสั่งได้จากเมธอด 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
จะทำให้เกิดปัญหาเล็กๆ น้อยๆ เนื่องจากคุณต้องเข้าถึงเมธอด address()
ในอินสแตนซ์ Instruction
คำตอบคือให้เพิ่ม instruction
ลงในการจับข้อมูล Lambda เพื่อให้พร้อมใช้งานในส่วนเนื้อหาของฟังก์ชัน Lambda แทนที่จะเป็น [](uint32_t a) { ...
}
ตามเดิม ควรเขียน lambda เป็น [instruction](uint32_t a) { ... }
ตอนนี้คุณใช้ instruction
ในส่วนเนื้อหาของ Lambda ได้แล้ว
ทำการเปลี่ยนแปลงและสร้างได้เลย คุณสามารถตรวจสอบงานของคุณเทียบกับ rv32i_instructions.cc
เพิ่มคำแนะนำในการเปลี่ยนขั้นตอนการควบคุม
คำสั่งสำหรับการเปลี่ยนแปลงขั้นตอนการควบคุมที่คุณต้องนำไปใช้จะแบ่งออกเป็นคำสั่ง Branch แบบมีเงื่อนไข (Branch ที่สั้นกว่าซึ่งจะดำเนินการหากการเปรียบเทียบเป็นจริง) และวิธีการข้ามลิงก์ ซึ่งใช้เพื่อเรียกใช้ฟังก์ชัน (และจะลบ -and-link ออกโดยการตั้งค่าการลงทะเบียนลิงก์เป็น 0 ทำให้การเขียนไม่ดำเนินการ)
เพิ่มวิธีการสาขาแบบมีเงื่อนไข
ไม่มีฟังก์ชันตัวช่วยสำหรับคำสั่ง Branch จึงมี 2 ตัวเลือก เขียนฟังก์ชันเชิงความหมายตั้งแต่ต้น หรือเขียนฟังก์ชันตัวช่วยในเครื่อง เนื่องจากเราต้องใช้คำสั่งสำหรับ Branch จำนวน 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();
}
}
สิ่งเดียวที่แตกต่างกันไปตามวิธีการของ Branch คือเงื่อนไข Branch และประเภทข้อมูลที่มีการลงนามกับ Int 32 บิตที่ไม่มีการรับรองของตัวถูกดำเนินการแหล่งที่มา 2 ตัว ซึ่งหมายความว่าเราต้องมีพารามิเตอร์เทมเพลตสำหรับโอเปอเรนด์ต้นทาง ฟังก์ชันตัวช่วยต้องใช้อินสแตนซ์ 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; });
}
คำสั่งสาขาที่เหลือมีดังนี้
- Beq - เงื่อนไขที่เท่ากัน
- Bgeu - สาขาที่มากกว่าหรือเท่ากับ (ไม่มีเครื่องหมาย)
- Blt - สาขาที่น้อยกว่า (ลงชื่อ)
- Bltu - เงื่อนไขที่น้อยกว่า (แบบไม่ลงนาม)
- Bne - สาขาไม่เท่ากัน
ทำการเปลี่ยนแปลงเพื่อใช้ฟังก์ชันความหมายเหล่านี้ แล้วสร้างใหม่ คุณสามารถตรวจสอบงานของคุณเทียบกับ rv32i_instructions.cc
เพิ่มวิธีการข้ามและลิงก์
การเขียนฟังก์ชันตัวช่วยสำหรับคำสั่งการข้ามและลิงก์นั้นไม่มีประโยชน์ เราจึงต้องเขียนฟังก์ชันเหล่านี้ขึ้นมาใหม่ เรามาเริ่มกันที่ ความหมายของการสอน
คำสั่ง jal
จะนําออฟเซตจากออปเรอเรนต์ต้นทาง 0 และเพิ่มลงใน PC (ที่อยู่คำสั่ง) ปัจจุบันเพื่อคํานวณเป้าหมายการกระโดด ระบบจะเขียนเป้าหมายการกระโดดไปยังออบเจ็กต์ปลายทาง 0 ที่อยู่สำหรับคืนสินค้าคือที่อยู่ของคำสั่งตามลำดับถัดไป ซึ่งสามารถคำนวณได้โดยเพิ่มขนาดของ
การสอนปัจจุบันลงในที่อยู่ ที่อยู่สำหรับคืนสินค้าจะถูกเขียนไปยัง
ตัวถูกดำเนินการปลายทาง 1 อย่าลืมใส่ตัวชี้ออบเจ็กต์คำสั่งใน
การจับภาพ lambda
คำสั่ง jalr
จะใช้รีจิสเตอร์ฐานเป็นออปเรอเรนด์ต้นทาง 0 และออฟเซตเป็นออปเรอเรนด์ต้นทาง 1 แล้วบวกเข้าด้วยกันเพื่อคํานวณเป้าหมายการกระโดด
มิเช่นนั้น คำสั่งจะเหมือนกับคำสั่ง jal
เขียนฟังก์ชันเชิงความหมาย 2 รายการและสร้างตามคำอธิบายเหล่านี้เกี่ยวกับความหมายของคำสั่ง คุณสามารถตรวจสอบงานของคุณเทียบกับ rv32i_instructions.cc
วิธีการเพิ่มการจัดเก็บหน่วยความจำ
วิธีการสำหรับร้านค้าที่เราต้องทำมี 3 อย่าง ได้แก่ ไบต์ร้านค้า (sb
) คำใน Store (sh
) และคำใน Store (sw
) โดยวิธีการของร้านค้าจะแตกต่างจากวิธีการที่เราใช้อยู่ คือไม่ได้เขียนเป็นสถานะของผู้ประมวลผลข้อมูลภายใน แต่จะเขียนไปยังทรัพยากรของระบบ
ซึ่งก็คือหน่วยความจำหลัก MPACT-Sim ไม่ถือว่าหน่วยความจำเป็นโอเปอแรนด์คำสั่ง ดังนั้นการเข้าถึงหน่วยความจำจึงต้องใช้วิธีการอื่น
คำตอบคือให้เพิ่มวิธีการเข้าถึงหน่วยความจำลงในออบเจ็กต์ ArchState
ของ MPACT-Sim หรือให้ถูกกว่านั้น ให้สร้างออบเจ็กต์สถานะ RiscV ใหม่ที่มาจาก ArchState
ซึ่งจะเพิ่มได้ ออบเจ็กต์ 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
เพื่อดำเนินการรั้ว
วิธีการสำหรับร้านค้าจะทำงานคล้ายกัน ก่อนอื่น ที่อยู่ที่มีประสิทธิภาพของการเข้าถึงหน่วยความจำจะคำนวณจากตัวถูกดำเนินการของแหล่งคำสั่งพื้นฐานและออฟเซ็ต จากนั้นค่าที่เก็บจะถูกดึงจากตัวถูกดำเนินการของแหล่งที่มาถัดไป ถัดไป ระบบจะรับออบเจ็กต์สถานะ RiscV ผ่านการเรียกเมธอด state()
และ static_cast<>
โดยจะเรียกใช้เมธอดที่เหมาะสม
เมธอด RiscVState
ของออบเจ็กต์ StoreMemory
ค่อนข้างเรียบง่าย แต่มีนัยบางอย่างที่ทำให้เราต้องระวัง ดังนี้
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
จะเห็นได้ว่าเมธอดจะใช้พารามิเตอร์ 3 รายการ ได้แก่ ตัวชี้ไปยังวิธีการของสโตร์ ที่อยู่ของร้านค้า และตัวชี้ไปยังอินสแตนซ์ DataBuffer
ที่มีข้อมูลร้านค้า โปรดทราบว่าไม่จำเป็นต้องระบุขนาด อินสแตนซ์ DataBuffer
เองมีเมธอด size()
อย่างไรก็ตาม จะไม่มีตัวถูกดำเนินการปลายทางที่เข้าถึงได้สำหรับคำสั่ง ซึ่งนำไปใช้จัดสรรอินสแตนซ์ DataBuffer
ของขนาดที่เหมาะสมได้ แต่เราต้องใช้DataBuffer
factory ที่ได้มาจากเมธอด db_factory()
ในอินสแตนซ์ 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
จะเข้าใจและจัดการกับกรณีนี้ เพื่อให้ Use Case ที่พบบ่อยที่สุดมีความซับซ้อนน้อยที่สุด แต่ StoreMemory
ไม่ได้เขียนแบบนั้น ระบบจะIncRef
อินสแตนซ์ DataBuffer
ขณะดำเนินการในอินสแตนซ์นั้น แล้ว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();
}
ไปทำฟังก์ชันอรรถศาสตร์ของ Store ให้เสร็จแล้วสร้างให้เสร็จเลย คุณตรวจสอบงานได้โดยเทียบกับ rv32i_instructions.cc
เพิ่มวิธีการโหลดหน่วยความจำ
คำแนะนำการโหลดที่จำเป็นต้องดำเนินการมีดังต่อไปนี้
lb
- โหลดไบต์ แล้วขยายลงในคำlbu
- โหลดไบต์แบบไม่ลงนาม ขยายเป็นคำแบบ 0lh
- โหลดครึ่งเวิร์ด ขยายขนาดลงท้ายด้วยเครื่องหมายเป็นเวิร์ดlhu
- โหลดคำที่ไม่มีเครื่องหมายครึ่งคำ ขยายเป็นศูนย์ในคำlw
- โหลดคำ
คำสั่งโหลดเป็นคำสั่งที่ซับซ้อนที่สุดที่เราต้องทำโมเดลในบทแนะนำนี้ คำสั่งเหล่านี้คล้ายกับคำสั่งจัดเก็บตรงที่จำเป็นต้องเข้าถึงออบเจ็กต์ RiscVState
แต่เพิ่มความซับซ้อนตรงที่คำสั่งโหลดแต่ละรายการจะแบ่งออกเป็นฟังก์ชันเชิงความหมาย 2 รายการแยกกัน คำสั่งแรกคล้ายกับคำสั่ง Store ตรงที่คำนวณที่อยู่ที่มีผลและเริ่มการเข้าถึงหน่วยความจำ รายการที่ 2 จะทำงานเมื่อเข้าถึงหน่วยความจำเสร็จสมบูรณ์ และเขียนข้อมูลหน่วยความจำไปยังตัวถูกดำเนินการปลายทางการลงทะเบียน
มาเริ่มกันที่การประกาศเมธอด LoadMemory
ใน RiscVState
void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
Instruction *child_inst, ReferenceCount *context);
เมื่อเทียบกับเมธอด StoreMemory
นั้น LoadMemory
จะใช้พารามิเตอร์เพิ่มเติมอีก 2 รายการ ได้แก่ ตัวชี้ไปยังอินสแตนซ์ Instruction
และตัวชี้ไปยังการอ้างอิงที่นับออบเจ็กต์ context
วิธีแรกคือคำสั่งสำหรับย่อยที่ใช้การเขียนบันทึกการจดทะเบียน (อธิบายไว้ในบทแนะนำเกี่ยวกับตัวถอดรหัส ISA) มีการเข้าถึงโดยใช้เมธอด child()
ในอินสแตนซ์ Instruction
ปัจจุบัน
ส่วนรายการหลังคือพอยน์เตอร์ไปยังอินสแตนซ์ของคลาสที่มาจาก ReferenceCount
ซึ่งในกรณีนี้จัดเก็บอินสแตนซ์ DataBuffer
ที่จะมีข้อมูลที่โหลด ออบเจ็กต์บริบทจะพร้อมใช้งานผ่านเมธอด context()
ในแอบเจ็กต์ Instruction
(แต่สําหรับคําสั่งส่วนใหญ่ ระบบจะตั้งค่าเป็น 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 เท่านั้น มาสร้างฟังก์ชันตัวช่วยที่ใช้เทมเพลตสำหรับคำสั่งการโหลดหลักกัน คำสั่งนี้จะคล้ายกับวิธีการของ Store มาก เพียงแต่จะไม่เข้าถึงตัวถูกดำเนินการต้นทางเพื่อรับค่า และจะสร้างออบเจ็กต์บริบท
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 ระบบจะอ่านค่า (ตามประเภทข้อมูล) จากอินสแตนซ์ load-data 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
ซึ่งจะแสดง
Command Shell แบบง่าย พิมพ์ 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] >
บทแนะนํานี้จบแล้ว เราหวังว่าข้อมูลนี้จะเป็นประโยชน์