ตัวถอดรหัสแบบผสานรวมของ RiscV

วัตถุประสงค์ของบทแนะนำนี้ได้แก่

  • เรียนรู้ว่า 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 &reg_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 &reg_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_));
  };
}

หากคุณต้องการความช่วยเหลือ (หรือต้องการตรวจสอบงาน) คำตอบแบบเต็มคือ ที่นี่

บทแนะนำนี้จบลงเพียงเท่านี้ เราหวังว่าข้อมูลนี้จะเป็นประโยชน์สำหรับคุณ