วัตถุประสงค์ของบทแนะนำนี้ได้แก่
- เรียนรู้ว่า ISA และเครื่องมือถอดรหัสแบบไบนารีที่สร้างขึ้นมาทำงานร่วมกันได้อย่างไร
- เขียนโค้ด C++ ที่จำเป็นเพื่อสร้างตัวถอดรหัสคำสั่งแบบเต็มสำหรับ RiscV RV32I ที่รวม ISA กับตัวถอดรหัสแบบไบนารี
ทำความเข้าใจตัวถอดรหัสคำสั่ง
ตัวถอดรหัสคำสั่งมีหน้าที่อ่าน รู้ที่อยู่คำสั่ง
คำคำแนะนำจากหน่วยความจำ และแสดงผลอินสแตนซ์เริ่มต้นแบบเต็มของ
Instruction
ที่แสดงถึงคำสั่งนั้น
ตัวถอดรหัสระดับบนสุดจะใช้ generic::DecoderInterface
ที่แสดงด้านล่าง
// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
public:
// Return a decoded instruction for the given address. If there are errors
// in the instruciton decoding, the decoder should still produce an
// instruction that can be executed, but its semantic action function should
// set an error condition in the simulation when executed.
virtual Instruction *DecodeInstruction(uint64_t address) = 0;
virtual ~DecoderInterface() = default;
};
จะเห็นได้ว่ามีเพียงวิธีการเดียวที่จะต้องใช้คือ cpp
virtual Instruction *DecodeInstruction(uint64_t address);
มาดูกันว่าโค้ดที่สร้างขึ้นมีอะไรบ้างและต้องใช้อะไรบ้าง
ขั้นแรก ให้พิจารณาคลาส RiscV32IInstructionSet
ระดับบนสุดในไฟล์
riscv32i_decoder.h
ซึ่งสร้างขึ้นในตอนท้ายของบทแนะนำเกี่ยวกับ
ตัวถอดรหัส ISA หากต้องการดูเนื้อหานั้นอีกครั้ง ให้ไปที่ไดเรกทอรีโซลูชันของ
บทแนะนำดังกล่าว
แล้วสร้างใหม่ทั้งหมด
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
ตอนนี้ให้เปลี่ยนไดเรกทอรีกลับไปเป็นรูทของที่เก็บ แล้วมาดูกัน
ในแหล่งที่มาที่สร้างขึ้น ในกรณีนี้ ให้เปลี่ยนไดเรกทอรีเป็น
bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(สมมติว่าคุณใช้ x86
host - สำหรับโฮสต์อื่น k8-fastbuild จะเป็นอีกสตริงหนึ่ง)
$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
คุณจะเห็นไฟล์ต้นฉบับ 4 ไฟล์ที่มีโค้ด C++ ที่สร้างขึ้นแสดงอยู่
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
เปิดไฟล์แรก riscv32i_decoder.h
เรามี 3 ชั้นเรียนที่เรา
ที่ควรตรวจสอบในเรื่องต่อไปนี้
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
จดชื่อชั้นเรียนไว้ ระบบจะตั้งชื่อชั้นเรียนทั้งหมดตาม
ชื่อแบบ Pascal ของชื่อที่ระบุใน "isa" การประกาศในไฟล์นั้น
isa RiscV32I { ... }
เรามาเริ่มกันที่ชั้นเรียน RiscVIInstructionSet
กันก่อน ซึ่งจะแสดงอยู่ด้านล่าง
class RiscV32IInstructionSet {
public:
RiscV32IInstructionSet(ArchState *arch_state,
RiscV32IInstructionSetFactory *factory);
Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);
private:
std::unique_ptr<Riscv32Slot> riscv32_decoder_;
ArchState *arch_state_;
};
ชั้นเรียนนี้ไม่มีวิธีการเสมือนจริง ดังนั้นชั้นเรียนนี้จึงเป็นชั้นเรียนเดี่ยวๆ
คุณจะสังเกตเห็น 2 สิ่ง ขั้นแรก ตัวสร้างจะชี้ไปยังอินสแตนซ์ของ
RiscV32IInstructionSetFactory
ชั้นเรียน นี่คือคลาสที่ที่สร้างขึ้น
ตัวถอดรหัสใช้เพื่อสร้างอินสแตนซ์ของคลาส RiscV32Slot
ซึ่งใช้เพื่อ
ถอดรหัสคำสั่งทั้งหมดที่กำหนดไว้สำหรับ slot RiscV32
ตามที่กำหนดไว้ใน
riscv32i.isa
ไฟล์ วิธีที่ 2 เมธอด Decode
จะใช้พารามิเตอร์เพิ่มเติม
ประเภทตัวชี้ไปยัง RiscV32IEncodingBase
นี่เป็นคลาสที่จะให้
อินเทอร์เฟซระหว่างตัวถอดรหัส isa ที่สร้างขึ้นในบทแนะนำแรกและไบนารี
ที่สร้างขึ้นในห้องทดลองที่ 2
คลาส RiscV32IInstructionSetFactory
เป็นคลาสนามธรรมที่เรา
จะต้องนำมาใช้กับตัวถอดรหัสแบบเต็ม ในกรณีส่วนมาก
นั้นน้อยมาก แค่บอกวิธีเรียกเครื่องมือสร้างสำหรับ
คลาสช่องโฆษณาที่กำหนดไว้ในไฟล์ .isa
ของเรา ในกรณีของเรา มันง่ายมากตรงที่มี
เป็นคลาสดังกล่าวเพียงคลาสเดียว: Riscv32Slot
(ตัวอักษรพิมพ์เล็กหรือใหญ่ของชื่อ riscv32
)
เชื่อมต่อกับ Slot
) ระบบไม่ได้สร้างวิธีการนี้ให้คุณเนื่องจากมี
กรณีการใช้งานขั้นสูงบางส่วนที่อาจมีประโยชน์ในการสร้างคลาสย่อย
จากช่อง และเรียกใช้ตัวสร้างแทน
เราจะเข้าสู่ชั้นเรียนสุดท้ายในอีก RiscV32IEncodingBase
ช่วงท้ายนี้
บทแนะนำ เนื่องจากเป็นหัวข้อของแบบฝึกหัดอื่น
กำหนดตัวถอดรหัสคำสั่งระดับบนสุด
กำหนดคลาสของโรงงาน
หากคุณสร้างโปรเจ็กต์อีกครั้งสำหรับบทแนะนำแรก อย่าลืมเปลี่ยนกลับไปใช้
ไดเรกทอรี riscv_full_decoder
เปิดไฟล์ riscv32_decoder.h
ไฟล์ที่จำเป็นทั้งหมดจะมี
เพิ่มและตั้งค่าเนมสเปซแล้ว
หลังจากความคิดเห็นที่ระบุว่า //Exercise 1 - step 1
ให้กำหนดชั้นเรียน
RiscV32IsaFactory
รับช่วงค่าจาก RiscV32IInstructionSetFactory
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
ถัดไป ให้กำหนดการลบล้างสำหรับ CreateRiscv32Slot
เนื่องจากเราไม่ได้ใช้
คลาสที่ดึงมาของ Riscv32Slot
เราเพียงจัดสรรอินสแตนซ์ใหม่โดยใช้
std::make_unique
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
หากคุณต้องการความช่วยเหลือ (หรือต้องการตรวจสอบงาน) คำตอบแบบเต็มคือ ที่นี่
กำหนดคลาสตัวถอดรหัส
การประกาศเครื่องมือสร้าง ตัวทำลาย และเมธอด
ต่อไปถึงเวลากำหนดคลาสตัวถอดรหัส ในไฟล์เดียวกันกับด้านบน ให้ไปที่ส่วน
การประกาศของRiscV32Decoder
ขยายการประกาศเป็นคําจํากัดความของชั้นเรียน
โดยที่ RiscV32Decoder
รับค่ามาจาก generic::DecoderInterface
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
ต่อไป ก่อนที่เราจะเขียนตัวสร้าง เรามาดูโค้ดคร่าวๆ กัน
ที่สร้างขึ้นในบทแนะนำที่ 2 เกี่ยวกับตัวถอดรหัสไบนารี นอกจากทุกอย่าง
Extract
จะมีฟังก์ชัน DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
ฟังก์ชันนี้จะใช้คำที่แนะนำที่ต้องถอดรหัส แล้วส่งกลับ
รหัสการดำเนินการที่ตรงกับคำสั่งนั้น ในทางกลับกัน ฟังก์ชัน
คลาส DecodeInterface
ที่ RiscV32Decoder
ใช้เฉพาะบัตรผ่านใน
อีเมล ดังนั้น คลาส RiscV32Decoder
จะต้องเข้าถึงหน่วยความจำเพื่อ
อ่านคำวิธีการเพื่อส่งไปยัง DecodeRiscVInst32()
ในโปรเจ็กต์นี้
วิธีเข้าถึงหน่วยความจำคือผ่าน อินเทอร์เฟซหน่วยความจำที่เรียบง่าย ที่กำหนดไว้ใน
.../mpact/sim/util/memory
ได้รับการตั้งชื่ออย่างเหมาะสมว่า util::MemoryInterface
ดังที่แสดงด้านล่าง
// Load data from address into the DataBuffer, then schedule the Instruction
// inst (if not nullptr) to be executed (using the function delay line) with
// context. The size of the data access is based on size of the data buffer.
virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
ReferenceCount *context) = 0;
นอกจากนี้ เราจำเป็นต้องส่งอินสแตนซ์คลาส state
ไปยัง
ของคลาสตัวถอดรหัสอื่นๆ ระดับรัฐที่เหมาะสมคือ
คลาส riscv::RiscVState
ซึ่งมาจาก generic::ArchState
โดยเพิ่ม
สำหรับ RiscV ซึ่งหมายความว่าเราจะต้องประกาศตัวสร้างเพื่อให้
สามารถนำตัวชี้ไปยัง state
และ memory
:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
ลบตัวสร้างเริ่มต้นและลบล้างตัวทำลายดังนี้
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
ถัดไปให้ประกาศเมธอด DecodeInstruction
ที่เราต้องลบล้าง
generic::DecoderInterface
generic::Instruction *DecodeInstruction(uint64_t address) override;
หากคุณต้องการความช่วยเหลือ (หรือต้องการตรวจสอบงาน) คำตอบแบบเต็มคือ ที่นี่
คำจำกัดความของสมาชิกข้อมูล
คลาส RiscV32Decoder
จะต้องมีสมาชิกข้อมูลส่วนตัวเพื่อจัดเก็บ
พารามิเตอร์ตัวสร้าง และตัวชี้ไปยังคลาสของโรงงาน
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
นอกจากนี้ยังต้องการตัวชี้ไปยังคลาสการเข้ารหัสที่ได้มาจาก
RiscV32IEncodingBase
สมมติว่าชื่อ RiscV32IEncoding
(เราจะใช้
ในแบบฝึกหัดที่ 2) นอกจากนี้ ต้องมีตัวชี้ไปยังอินสแตนซ์
RiscV32IInstructionSet
ดังนั้นให้เพิ่ม:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
สุดท้าย เราต้องกำหนดสมาชิกข้อมูลที่จะใช้กับอินเทอร์เฟซหน่วยความจำของเรา ดังนี้
generic::DataBuffer *inst_db_;
หากคุณต้องการความช่วยเหลือ (หรือต้องการตรวจสอบงาน) คำตอบแบบเต็มคือ ที่นี่
กำหนดเมธอดคลาสตัวถอดรหัส
ต่อไปก็ถึงเวลาใช้ตัวสร้าง ตัวทำลาย และ
DecodeInstruction
วิธี เปิดไฟล์ riscv32_decoder.cc
ว่างเปล่า
อยู่ในไฟล์ตลอดจนการประกาศเนมสเปซแล้ว
ของการประกาศ using
รายการ
คำจำกัดความของเครื่องมือสร้าง
เครื่องมือสร้างเพียงต้องเริ่มต้นสมาชิกข้อมูลเท่านั้น ก่อนอื่น
state_
และ memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
ถัดไปให้จัดสรรอินสแตนซ์ของแต่ละคลาสที่เกี่ยวข้องกับตัวถอดรหัส โดยส่งผ่านใน พารามิเตอร์ที่เหมาะสม
// Allocate the isa factory class, the top level isa decoder instance, and
// the encoding parser.
riscv_isa_factory_ = new RiscV32IsaFactory();
riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
riscv_encoding_ = new RiscV32IEncoding(state);
จัดสรรอินสแตนซ์ DataBuffer
ขั้นสุดท้าย ได้รับการจัดสรรโดยใช้โรงงาน
เข้าถึงได้ผ่านสมาชิก state_
เราจัดสรรขนาดบัฟเฟอร์ข้อมูลเพื่อจัดเก็บ
uint32_t
เดียว เนื่องจากเป็นขนาดของคำที่แนะนำ
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
คำจำกัดความของตัวทำลาย
เครื่องมือทำลายนั้นง่ายมาก เพียงแค่ปล่อยวัตถุที่เราจัดสรรไว้ในตัวสร้าง
ด้วยการหักมุมเพียงครั้งเดียว อินสแตนซ์บัฟเฟอร์ข้อมูลจะถูกนับการอ้างอิง ดังนั้น
ที่ไม่เรียกใช้ delete
ในเคอร์เซอร์ เราจะDecRef()
ออบเจ็กต์:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
คำจำกัดความของเมธอด
ในกรณีของเรา การใช้วิธีการนี้ค่อนข้างง่าย เราจะถือว่า ที่อยู่มีการจัดแนวที่ถูกต้อง และไม่มีการตรวจสอบข้อผิดพลาดเพิ่มเติม ต้องระบุ
ก่อนอื่น จะต้องดึงคำที่แนะนำจากหน่วยความจำโดยใช้หน่วยความจำ
ของอินเทอร์เฟซ และอินสแตนซ์ DataBuffer
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
ถัดไป เราจะเรียกใช้อินสแตนซ์ RiscVIEncoding
เพื่อแยกวิเคราะห์คำคำสั่ง
ซึ่งจะต้องทำก่อนที่จะเรียกใช้ตัวถอดรหัส ISA จำได้ว่า ISA
ตัวถอดรหัสเรียกใช้อินสแตนซ์ RiscVIEncoding
โดยตรงเพื่อรับ opcode
และตัวถูกดำเนินการที่ระบุโดยคำที่แนะนำ เรายังไม่ได้นำวิธีการดังกล่าวมาใช้
แต่ลองใช้ void ParseInstruction(uint32_t)
เป็นวิธีนั้น
riscv_encoding_->ParseInstruction(iword);
สุดท้ายเราเรียกว่าตัวถอดรหัส ISA ซึ่งส่งผ่านที่อยู่และคลาสการเข้ารหัส
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
หากคุณต้องการความช่วยเหลือ (หรือต้องการตรวจสอบงาน) คำตอบที่สมบูรณ์คือ ที่นี่
คลาสการเข้ารหัส
คลาสการเข้ารหัสจะใช้อินเทอร์เฟซที่คลาสตัวถอดรหัสใช้ เพื่อรับรหัสการดำเนินการ ตัวถูกดำเนินการต้นทางและปลายทาง และ ตัวถูกดำเนินการของทรัพยากร ออบเจ็กต์เหล่านี้ทั้งหมดขึ้นอยู่กับข้อมูลจากไบนารี ตัวถอดรหัสรูปแบบ เช่น opcode ค่าของฟิลด์ที่เฉพาะเจาะจงใน คำที่แนะนำ เป็นต้น ซึ่งจะแยกออกจากคลาสตัวถอดรหัสเพื่อเก็บไว้ ไม่จำเป็นต้องเข้ารหัสและรองรับรูปแบบการเข้ารหัสหลากหลายรูปแบบ ในอนาคต
RiscV32IEncodingBase
เป็นคลาสนามธรรม เราต้องเลือกใช้
ในคลาสที่ได้รับมาแสดงอยู่ด้านล่างนี้
class RiscV32IEncodingBase {
public:
virtual ~RiscV32IEncodingBase() = default;
virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;
virtual ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end) = 0;
virtual ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end) = 0;
virtual PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op) = 0;
virtual SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no) = 0;
virtual DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency) = 0;
virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no) = 0;
};
ตอนแรกดูซับซ้อนเล็กน้อย โดยเฉพาะกับจำนวน แต่สำหรับสถาปัตยกรรมง่ายๆ อย่าง RiscV แล้ว พารามิเตอร์ เนื่องจากค่าของพารามิเตอร์จะบอกเป็นนัย
มาดูแต่ละวิธีตามลำดับ
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
เมธอด GetOpcode
จะแสดงสมาชิก OpcodeEnum
ของรายการปัจจุบัน
โดยระบุรหัสการดำเนินการ ชั้นเรียน OpcodeEnum
คือ
ที่กำหนดไว้ในไฟล์ตัวถอดรหัส isa riscv32i_enums.h
ที่สร้างขึ้น วิธีการใช้เวลา
พารามิเตอร์สองตัว ซึ่งสามารถละเว้นสำหรับทั้ง 2 พารามิเตอร์เพื่อจุดประสงค์ของเรา ครั้งแรกของ
นี่คือประเภทช่องโฆษณา (คลาส enum ที่กำหนดใน riscv32i_enums.h
ด้วย)
ซึ่งเนื่องจาก RiscV มีเพียงช่องเดียวเท่านั้น จึงมีค่าที่เป็นไปได้เพียงค่าเดียว
SlotEnum::kRiscv32
อย่างที่สองคือหมายเลขอินสแตนซ์ของช่อง (ในกรณีที่
สล็อตแมชชีนมีหลายอินสแตนซ์ ซึ่งอาจเกิดขึ้นใน VLIW บางรุ่น
สถาปัตยกรรม)
ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end)
ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end);
อีก 2 วิธีถัดไปจะใช้ในการสร้างแบบจำลองทรัพยากรฮาร์ดแวร์ในโปรเซสเซอร์
เพื่อปรับปรุงความแม่นยำของรอบ สำหรับแบบฝึกหัดในบทแนะนำ เราจะไม่ใช้
ดังนั้นเมื่อติดตั้งใช้งาน แท็กจะถูกตัดออกและส่งกลับ nullptr
PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op);
SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no);
DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency);
ทั้ง 3 วิธีนี้จะส่งคืนตัวชี้ไปยังวัตถุตัวถูกดำเนินการที่ใช้ภายใน
ฟังก์ชันอรรถศาสตร์การสอนเพื่อเข้าถึงค่าของคำสั่งใดๆ
ตัวถูกดำเนินการของคำสั่ง ตัวถูกดำเนินการของแหล่งที่มาของคำสั่งแต่ละรายการ และเขียนใหม่
ลงในตัวถูกดำเนินการปลายทางของวิธีการ เนื่องจาก RiscV ไม่ได้ใช้
คำสั่งที่แสดง เมธอดนั้นจะต้องแสดงผล nullptr
เท่านั้น
รูปแบบของพารามิเตอร์จะคล้ายกันในฟังก์ชันเหล่านี้ ขั้นแรก เช่น
GetOpcode
ช่องโฆษณาและรายการจะถูกส่งเข้ามา จากนั้นรหัสการดำเนินการสำหรับ
คำสั่งสำหรับสร้างตัวถูกดำเนินการ โดยจะใช้เฉพาะในกรณีที่
รหัสดำเนินการที่ต่างกันต้องส่งคืนวัตถุถูกดำเนินการที่แตกต่างกันสำหรับตัวถูกดำเนินการเดียวกัน
แต่ไม่ใช่กรณีปกติ
สำหรับเครื่องมือจำลอง RiscV นี้
ถัดไปเป็นรายการการระบุโอเปอแรนด์ ต้นทาง และปลายทาง
ระบุตัวถูกดำเนินการที่ต้องสร้าง ซึ่งมาจาก
OpEnums ใน riscv32i_enums.h
ดังที่แสดงด้านล่าง
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
หากมองย้อนกลับไปที่
riscv32.isa
คุณจะสังเกตเห็นว่าค่าเหล่านี้สอดคล้องกับชุดของต้นทางและปลายทาง
ชื่อตัวถูกดำเนินการที่ใช้ในการประกาศของคำสั่งแต่ละข้อ โดยการใช้
ชื่อตัวถูกดำเนินการของตัวถูกดำเนินการที่แสดงถึงบิตฟิลด์และตัวถูกดำเนินการต่างๆ
ก็จะทำให้การเขียนคลาสการเข้ารหัสนั้นง่ายขึ้น เนื่องจากสมาชิก enum ไม่ซ้ำกัน
กำหนดประเภทตัวถูกดำเนินการที่แน่นอนที่จะส่งคืน และไม่จำเป็นต้อง
พิจารณาค่าของพารามิเตอร์ช่อง รายการ หรือ opcode
สุดท้าย สำหรับตัวถูกดำเนินการต้นทางและปลายทาง ตำแหน่งตามลำดับของ ตัวถูกดำเนินการจะส่งไปใน (เราไม่ต้องสนใจข้อความนี้เช่นกัน) และสำหรับปลายทาง ตัวถูกดำเนินการ เวลาในการตอบสนอง (เป็นรอบ) ที่ผ่านไประหว่างเวลาที่คำสั่ง และผลลัพธ์ปลายทางจะพร้อมให้คำแนะนำในลำดับต่อมา ในเครื่องจำลองของเรา เวลาในการตอบสนองนี้จะเป็น 0 ซึ่งหมายความว่าคำแนะนำจะเขียนว่า ให้ผู้ลงทะเบียนทราบทันที
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
ใช้ฟังก์ชันสุดท้ายเพื่อรับเวลาในการตอบสนองของปลายทางที่เฉพาะเจาะจง
ตัวถูกดำเนินการหากระบุเป็น *
ในไฟล์ .isa
กรณีนี้เกิดขึ้นได้น้อยมาก
และไม่ได้ใช้สำหรับเครื่องมือจำลอง RiscV นี้ ดังนั้นการใช้งานฟังก์ชันนี้ของเรา
จะแสดงผลเป็น 0
กำหนดคลาสการเข้ารหัส
ไฟล์ส่วนหัว (.h)
เมธอด
เปิดไฟล์ riscv32i_encoding.h
ไฟล์ที่จำเป็นทั้งหมดจะมี
เพิ่มและตั้งค่าเนมสเปซแล้ว การเพิ่มรหัสทั้งหมดคือ
ติดตามความคิดเห็น // Exercise 2.
เรามาเริ่มต้นด้วยการกำหนดคลาส RiscV32IEncoding
ที่รับค่าจาก
ที่สร้างขึ้นโดยอัตโนมัติ
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
ถัดไป ตัวสร้างควรใช้ตัวชี้ไปยังอินสแตนซ์สถานะ ซึ่งในกรณีนี้
ตัวชี้ไปยัง riscv::RiscVState
ควรใช้ตัวทำลายที่เป็นค่าเริ่มต้น
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
ก่อนที่เราจะเพิ่มเมธอดอินเทอร์เฟซทั้งหมด เรามาเพิ่มเมธอดที่เรียกโดย
RiscV32Decoder
เพื่อแยกวิเคราะห์คำสั่ง
void ParseInstruction(uint32_t inst_word);
ถัดไป ให้เพิ่มเมธอดที่มีการลบล้างเพียงเล็กน้อยในขณะที่ยกเลิก ชื่อของพารามิเตอร์ที่ไม่ใช้งาน:
// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
SimpleResourceVector &,
int) override {
return nullptr;
}
ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
ComplexResourceEnum ,
int, int) override {
return nullptr;
}
PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
PredOpEnum) override {
return nullptr;
}
int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }
สุดท้าย ให้เพิ่มการแทนที่เมธอดที่เหลือของอินเทอร์เฟซสาธารณะแต่มี เลื่อนการนำไปใช้งานในไฟล์ .cc
OpcodeEnum GetOpcode(SlotEnum, int) override;
SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
SourceOpEnum source_op, int) override;
DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
DestOpEnum dest_op, int,
int latency) override;
เพื่อลดความซับซ้อนในการติดตั้งใช้งานเมธอด Getter ของตัวถูกดำเนินการแต่ละรายการ
เราจะสร้าง callables (ออบเจ็กต์ฟังก์ชัน) 2 อาร์เรย์ที่จัดทำดัชนีโดย
ตัวเลขของสมาชิก SourceOpEnum
และ DestOpEnum
ตามลำดับ
ด้วยวิธีนี้ เนื้อหาในหลักเหล่านี้จะลดลงเหลือเพียงการเรียกใช้
ฟังก์ชันสำหรับค่า enum ที่ส่งผ่านและส่งกลับค่า
ในการจัดการการเริ่มต้นของ 2 อาร์เรย์เหล่านี้ เรากำหนดฟิลด์ส่วนตัว 2 รายการ ที่จะถูกเรียกจากเครื่องมือสร้างดังต่อไปนี้
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
สมาชิกข้อมูล
สมาชิกข้อมูลที่ต้องมีมีดังนี้
state_
เพื่อเก็บค่าriscv::RiscVState *
inst_word_
ประเภทuint32_t
ซึ่งมีค่าของแอตทริบิวต์ปัจจุบัน ข้อความคำสั่งopcode_
เพื่อเก็บรหัสของคำสั่งปัจจุบันซึ่งอัปเดตโดย เมธอดParseInstruction
ประเภทนี้มีประเภทOpcodeEnum
source_op_getters_
อาร์เรย์ที่จะจัดเก็บCallable ที่ใช้เพื่อรับแหล่งที่มา อ็อบเจ็กต์ของตัวถูกดำเนินการ ประเภทขององค์ประกอบอาร์เรย์คือabsl::AnyInvocable<SourceOperandInterface *>()>
dest_op_getters_
อาร์เรย์สำหรับจัดเก็บCallable ที่ใช้เพื่อรับ ออบเจ็กต์ตัวถูกดำเนินการปลายทาง ประเภทขององค์ประกอบอาร์เรย์คือabsl::AnyInvocable<DestinationOperandInterface *>()>
xreg_alias
อาร์เรย์ของจำนวนเต็ม RiscV ที่จดทะเบียนชื่อ ABI เช่น "ศูนย์" และ "ra" แทนที่จะเป็น "x0" และ "x1"
riscv::RiscVState *state_;
uint32_t inst_word_;
OpcodeEnum opcode_;
absl::AnyInvocable<SourceOperandInterface *()>
source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
absl::AnyInvocable<DestinationOperandInterface *(int)>
dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];
const std::string xreg_alias_[32] = {
"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0",
"a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5",
"s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};
หากคุณต้องการความช่วยเหลือ (หรือต้องการตรวจสอบงาน) คำตอบแบบเต็มคือ ที่นี่
ไฟล์ต้นฉบับ (.cc)
เปิดไฟล์ riscv32i_encoding.cc
ไฟล์ที่จำเป็นทั้งหมดจะมี
เพิ่มและตั้งค่าเนมสเปซแล้ว การเพิ่มรหัสทั้งหมดคือ
ติดตามความคิดเห็น // Exercise 2.
ฟังก์ชันตัวช่วย
เราจะเริ่มด้วยการเขียนฟังก์ชันผู้ช่วย 2 อย่างที่เราใช้ในการสร้าง
ตัวถูกดำเนินการของการลงทะเบียนต้นทางและปลายทาง ซึ่งจะมีเทมเพลตใน
ประเภทการลงทะเบียน และจะเรียกใช้ออบเจ็กต์ RiscVState
เพื่อรับแฮนเดิล
ลงทะเบียนวัตถุ จากนั้นเรียกเมธอดโรงงานโอเปอแรนด์ในวัตถุการลงทะเบียน
มาเริ่มกันที่ตัวช่วยเหลือตัวถูกดำเนินการปลายทาง
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency);
}
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency, op_name);
}
คุณจะเห็นว่ามีฟังก์ชันตัวช่วย 2 อย่าง ส่วนที่สองใช้เวลา
พารามิเตอร์ op_name
ที่อนุญาตให้ตัวถูกดำเนินการมีชื่อหรือสตริงที่แตกต่างกัน
มากกว่าตัวจดทะเบียนที่เกี่ยวข้อง
ในทำนองเดียวกันสำหรับตัวช่วยเหลือตัวถูกดำเนินการต้นทาง
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand();
return op;
}
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand(op_name);
return op;
}
ฟังก์ชันตัวสร้างและอินเทอร์เฟซ
ตัวสร้างและฟังก์ชันอินเทอร์เฟซนั้นเรียบง่ายมาก เครื่องมือสร้าง จะเรียกใช้เมธอดเริ่มต้น 2 วิธีเพื่อเริ่มอาร์เรย์ callables สําหรับ ตัวถูกดำเนินการ
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
จัดเก็บคำที่แนะนำแล้วตามด้วยรหัสการดำเนินการที่คำนั้น
ได้มาจากการเรียกใช้โค้ดที่สร้างโดยตัวถอดรหัสไบนารี
// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
inst_word_ = inst_word;
opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}
สุดท้าย ตัวถูกดำเนินการของตัวถูกดำเนินการจะส่งคืนค่าจากฟังก์ชัน Getter ที่เรียกใช้ ตามการค้นหาอาร์เรย์โดยใช้ค่า Enum ของตัวถูกดำเนินการปลายทาง/ต้นทาง
DestinationOperandInterface *RiscV32IEncoding::GetDestination(
SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
return dest_op_getters_[static_cast<int>(dest_op)](latency);
}
SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
SourceOpEnum source_op, int) {
return source_op_getters_[static_cast<int>(source_op)]();
}
วิธีการเริ่มต้นอาร์เรย์
คุณคงเดาได้ว่างานส่วนใหญ่คือการเริ่มต้น getter
แต่ไม่ต้องกังวล สามารถทำได้โดยใช้รูปแบบที่ทำซ้ำได้ง่าย มาเริ่มกันเลย
ให้เริ่มต้นด้วย InitializeDestinationOpGetters()
ก่อน เนื่องจากมี
ตัวถูกดำเนินการปลายทาง 2 ตัว
เรียกคืนชั้นเรียน DestOpEnum
ที่สร้างขึ้นจาก riscv32i_enums.h
:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
สำหรับ dest_op_getters_
เราต้องเริ่มต้น 4 รายการ โดยแต่ละรายการมีไว้สำหรับ kNone
kCsr
, kNextPc
และ kRd
เพื่อความสะดวก แต่ละรายการจะเริ่มด้วย
lambda แต่คุณสามารถใช้คำว่า callable รูปแบบอื่นๆ ได้เช่นกัน ลายเซ็น
ของแลมบ์ดาคือ void(int latency)
จนถึงตอนนี้ เราไม่ได้พูดถึงจุดหมายประเภทต่างๆ มากนัก
ตัวถูกดำเนินการที่กำหนดไว้ใน MPACT-Sim สำหรับแบบฝึกหัดนี้ เราจะใช้
ประเภท: generic::RegisterDestinationOperand
ที่กำหนดไว้ใน
register.h
,
และ generic::DevNullOperand
กำหนดไว้ใน
devnull_operand.h
รายละเอียดของตัวถูกดำเนินการเหล่านี้ยังไม่สำคัญในขณะนี้ ยกเว้นว่า
first ใช้เพื่อเขียนในการลงทะเบียน และรายหลังจะละเว้นการเขียนทั้งหมด
รายการแรกสำหรับ kNone
เป็นข้อมูลเพียงเล็กน้อย เพียงแสดงผล nullptr และหากต้องการ
บันทึกข้อผิดพลาด
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
เรื่องถัดไปมาจาก kCsr
เราจะโกงกันนิดหน่อย "สวัสดีโลก" โปรแกรม
ไม่ได้อาศัยการอัปเดต CSR จริง แต่มีโค้ดต้นแบบที่
ดำเนินการตามคำสั่ง CSR วิธีแก้ไขคือให้หลอกระบบโดยใช้
การจดทะเบียนปกติชื่อ "CSR" และกำหนดช่องทางการเขียนดังกล่าวทั้งหมด
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
ถัดไปคือ kNextPc
ซึ่งอ้างถึง "pc" ลงทะเบียน ซึ่งใช้เป็นเป้าหมาย
สำหรับ Branch และวิธีการข้ามทั้งหมด ชื่อจะกำหนดไว้ใน RiscVState
เป็น
kPcName
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
สุดท้ายคือตัวถูกดำเนินการปลายทาง kRd
ใน riscv32i.isa
ตัวถูกดำเนินการ
rd
ใช้เพื่ออ้างอิงถึงการลงทะเบียนจำนวนเต็มที่เข้ารหัสในส่วน "rd" เท่านั้น ฟิลด์
ของคำที่ใช้สอน เพื่อให้ไม่มีความกำกวมในความหมายของคำนั้น มี
เป็นเพียงข้อมูลแทรกเดียว จดทะเบียน x0
(ชื่อ Abi zero
) เดินสายเป็น 0,
ดังนั้นในการลงทะเบียน เราจะใช้ DevNullOperand
ดังนั้นใน getter นี้ เราจะแยกค่าในช่อง rd
โดยใช้ฟิลด์
สร้างเมธอด Extract
จากไฟล์ .bin_fmt แล้ว หากค่าเป็น 0 เราจะ
แสดงผล "DevNull" มิฉะนั้นเราจะส่งคืนตัวถูกดำเนินการการจดทะเบียนที่ถูกต้อง
ให้ใช้ชื่อแทนการจดทะเบียนที่เหมาะสมเป็นชื่อตัวถูกดำเนินการ
dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
// First extract register number from rd field.
int num = inst32_format::ExtractRd(inst_word_);
// For register x0, return the DevNull operand.
if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
// Return the proper register operand.
return GetRegisterDestinationOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
xreg_alias_[num]);
)
}
}
ตอนนี้ไปที่เมธอด InitializeSourceOperandGetters()
โดยที่รูปแบบคือ
เหมือนเดิม แต่มีรายละเอียดต่างกันเล็กน้อย
ก่อนอื่น มาดู SourceOpEnum
ที่สร้างขึ้นจาก
riscv32i.isa
ในบทแนะนำแรก:
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
การตรวจสอบสมาชิกนอกเหนือจาก kNone
แล้วแบ่งออกเป็น 2 กลุ่ม หนึ่ง
เป็นตัวถูกดำเนินการทันที: kBimm12
, kImm12
, kJimm20
, kSimm12
, kUimm20
และ kUimm5
อีกรายการคือตัวถูกดำเนินการลงทะเบียน: kCsr
, kRs1
และ kRs2
ตัวถูกดำเนินการ kNone
จะมีการจัดการเช่นเดียวกับตัวถูกดำเนินการปลายทาง - แสดงผล
nullptr
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
ต่อไป มาดูตัวถูกดำเนินการการลงทะเบียนกัน เราจะจัดการkCsr
กับวิธีที่เราจัดการกับตัวถูกดำเนินการปลายทางที่เกี่ยวข้อง เพียงเรียกใช้
ฟังก์ชันตัวช่วยที่ใช้ "CSR" เป็นชื่อทะเบียน
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
ตัวดำเนินการ kRs1
และ kRs2
จะได้รับการจัดการเทียบเท่ากับ kRd
ยกเว้นในกรณีที่
แม้ว่าเราจะไม่ต้องการอัปเดต x0
(หรือ zero
) แต่เราก็ต้องแน่ใจว่า
เราจะอ่าน 0 จากตัวถูกดำเนินการนั้นเสมอ โดยเราจะใช้
กำหนดคลาส generic::IntLiteralOperand<>
รายการใน
literal_operand.h
ตัวถูกดำเนินการนี้ใช้เพื่อเก็บค่าลิเทอรัล (ซึ่งตรงกันข้ามกับการจำลอง
ทันที) มิฉะนั้นรูปแบบจะเหมือนกัน: ก่อนอื่นให้แยกส่วน
ค่า rs1/rs2 จากคำที่แนะนำ หากเป็น 0 ให้แสดงผลลิเทอรัล
ตัวถูกดำเนินการด้วยพารามิเตอร์เทมเพลต 0 ไม่เช่นนั้นให้แสดงผลการลงทะเบียนปกติ
ตัวถูกดำเนินการต้นทางโดยใช้ฟังก์ชันตัวช่วย โดยใช้ชื่อแทน ABI เป็นตัวถูกดำเนินการ
ชื่อ
source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs1(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs2(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
ในขั้นสุดท้าย เราจะจัดการตัวถูกดำเนินการทันทีที่แตกต่างกัน ค่าทันทีคือ
จัดเก็บไว้ในอินสแตนซ์ของคลาส generic::ImmediateOperand<>
ที่กำหนดไว้ใน
immediate_operand.h
ความแตกต่างเพียงอย่างเดียวระหว่าง getters ต่างๆ สำหรับตัวถูกดำเนินการทันที
จะใช้ฟังก์ชันแยกข้อมูลใด และมีการรับรองประเภทพื้นที่เก็บข้อมูลหรือไม่
ไม่มีลายเซ็น ตามบิตฟิลด์
// Immediates.
source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractBImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractImm12(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm5(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractJImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractSImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm32(inst_word_));
};
}
หากคุณต้องการความช่วยเหลือ (หรือต้องการตรวจสอบงาน) คำตอบแบบเต็มคือ ที่นี่
บทแนะนำนี้จบลงเพียงเท่านี้ เราหวังว่าข้อมูลนี้จะเป็นประโยชน์สำหรับคุณ