جهاز فك الترميز المدمج 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 ، أما بالنسبة إلى المضيفين الآخرين، فستكون k8-fastbuild سلسلة أخرى).

$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder

ستظهر لك الملفات الأربعة التي تحتوي على رمز C++ الذي تم إنشاؤه:

  • riscv32i_decoder.h
  • riscv32i_decoder.cc
  • riscv32i_enums.h
  • riscv32i_enums.cc

افتح الملف الأول riscv32i_decoder.h. هناك ثلاث فئات يجب الاطّلاع عليها:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

دوِّن أسماء الفئات. تتم تسمية جميع الفئات استنادًا إلى نسخة Pascal-case من الاسم الوارد في بيان 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_;
};

لا تتضمّن هذه الفئة طرقًا افتراضية، لذا فهي فئة مستقلة، ولكن ينبغي ملاحظة أمرَين: أولاً، يأخذ المُنشئ مؤشرًا إلى مثيل من فئة RiscV32IInstructionSetFactory. هذه فئة يستخدمها رمز التفكيك الذي تم إنشاؤه لإنشاء مثيل لفئة RiscV32Slot، والتي يتم استخدامها لفك ترميز جميع التعليمات المحدّدة لفئة slot RiscV32 كما هو محدّد في ملف riscv32i.isa. ثانيًا، تأخذ طريقة Decode مَعلمة إضافية من النوع "مُشير إلى RiscV32IEncodingBase"، وهي فئة ستوفّر الواجهة بين وحدة ترميز isa التي تم إنشاؤها في البرنامج التعليمي الأول ووحدة ترميز الثنائية التي تم إنشاؤها في التجربة الثانية.

الفئة 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:
};

بعد ذلك، قبل أن نكتب الدالة الإنشائية، لنلقي نظرة سريعة على التعليمة البرمجية التي تم إنشاؤها في البرنامج التعليمي الثاني حول برنامج فك الترميز الثنائي. بالإضافة إلى كل الدوال 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_;

إذا كنت بحاجة إلى مساعدة (أو أردت التحقق من عملك)، يمكنك الحصول على الإجابة الكاملة هنا.

تحديد طرق فئة Decoder

بعد ذلك، حان وقت تنفيذ طريقة الإنشاء وطريقة التدمير وطريقة DecodeInstruction. افتح الملف riscv32_decoder.cc. إنّ ال methods الفارغة متوفرة في الملف بالإضافة إلى بيانات النطاق وإعلانين لـ 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 مباشرةً للحصول على رمز التشغيل والمَعلمات المحدَّدة في كلمة التعليمات. لم نقم بتنفيذ هذه الفئة بعد، ولكن دعنا نستخدم void ParseInstruction(uint32_t) بهذه الطريقة.

  riscv_encoding_->ParseInstruction(iword);

أخيرًا، نستدعي وحدة ترميز ISA، مع تمرير العنوان وفئة الترميز.

  auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
  return instruction;

إذا كنت بحاجة إلى مساعدة (أو أردت التحقق من عملك)، يمكنك الحصول على الإجابة الكاملة هنا.


فئة الترميز

تُنفِّذ فئة الترميز واجهة يستخدِمها فئة وحدة فك التشفير للحصول على رمز التشغيل للتعليمات وعناصر التشغيل المصدر والوجهة وعناصر التشغيل للموارد. تعتمد جميع هذه العناصر على معلومات من وحدة فك ترميز التنسيق الثنائي، مثل رمز التشغيل وقيم حقول معيّنة في كلمة التعليمات وما إلى ذلك. ويتم فصل هذا عن فئة وحدة فك الترميز للحفاظ على الاستقلال عن الترميز وتفعيل إتاحة خطط ترميز مختلفة متعددة في المستقبل.

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. تأخذ الطريقة مَعلمتَين يمكن تجاهلهما لأغراضنا. أول هذه العناصر هو نوع الفتحة (وهو فئة مصنّفة تم تحديدها أيضًا في 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);

تُستخدَم الطريقتان التاليتان لوضع نماذج لموارد الأجهزة في المعالج لتحسين دقة الدورة. في التمارين التعليمية، لن نستخدم هذه العناصر، لذا سيتم استبدالها في التنفيذ، ما يؤدي إلى عرض القيمة 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);

تعرض هذه الطرق الثلاث مؤشرات إلى عناصر المَعلمات التي يتم استخدامها ضمن الدوالّ الدلالية للتعليمات للوصول إلى قيمة أي مَعلمة قيد في التعليمة، وكل مَعلمة من مَعلمات مصدر التعليمة، وكتابة قيم جديدة في مَعلمات وجهة التعليمة. بما أنّ RiscV لا يستخدم عبارات الشروط للتعليمات، يجب أن تُرجع هذه الطريقة nullptr فقط.

يتشابه نمط المَعلمات في هذه الدوالّ. أولاً، تمامًا مثل GetOpcode، يتمّ تمرير الفتحة والإدخال. ثمّ رمز التشغيل لتعليمات التي يجب إنشاء الم Operand لها. لا يتم استخدام هذا الإجراء إلا إذا كانت تعليمات التشغيل المختلفة تحتاج إلى عرض عناصر مختلفة للمَعلمات لأنواع المَعلمات نفسها، وهذا ليس هو الحال في محاكي 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، ستلاحظ أنّ هذه الأسماء تتوافق مع مجموعات أسماء معاملات المصدر والوجهة المستخدمة في بيان كل تعليمات. باستخدام أسماء مختلفة للمَعلمات التي تمثّل حقول بت وأنواع مَعلمات مختلفة، يصبح من الأسهل كتابة فئة الترميز لأنّ العنصر المحدد في التعداد يحدِّد بشكل فريد نوع المَعلمة الدقيق الذي سيتم إرجاعه، وليس من الضروري النظر في قيم مَعلمات الفتحة أو الإدخال أو رمز التشغيل.

أخيرًا، بالنسبة إلى عاملَي التشغيل المصدر والوجهة، يتم تمرير الموضع الترتيبي لعامل التشغيل (مرة أخرى، يمكننا تجاهل ذلك)، وبالنسبة إلى عامل التشغيل الوجهة، يتم تمرير وقت الاستجابة (بالدورات) الذي يمر بين وقت إصدار التعليمات ونتيجة الوجهة، وتكون متاحة للتعليمات اللاحقة. في المحاكي، سيكون وقت الاستجابة هذا 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;

لتبسيط تنفيذ كل من طرق الحصول على الم Operand سننشئ صفيفَين من العناصر القابلة للاستدعاء (كائنات الدوال) مفهرَسَين بالقيمة الرقمية للعنصرَين SourceOpEnum وDestOpEnum على التوالي. بهذه الطريقة، يتم تقليل نصوص هاتين الطريقتَين إلى استدعاء كائن الدالة لقيمة التعداد التي يتم تمريرها وعرض قيمتها المعروضة.

لتنظيم عملية إعداد هاتين الصفيفتَين، نحدِّد وظيفتَين شقيقتَين ستتم استدعاؤهما من الدالة المُنشئ على النحو التالي:

 private:
  void InitializeSourceOperandGetters();
  void InitializeDestinationOperandGetters();

أعضاء البيانات

في ما يلي عناصر البيانات المطلوبة:

  • state_ للاحتفاظ بالقيمة riscv::RiscVState *
  • inst_word_ من النوع uint32_t الذي يحتوي على قيمة كلمة التعليمة الحالية.
  • opcode_ لتخزين رمز التشغيل للتعليمات الحالية التي يتم تعديلها باستخدام ParseInstruction من النوع OpcodeEnum.
  • source_op_getters_ مصفوفة لتخزين العناصر القابلة للاستدعاء المستخدَمة للحصول على كائنات معاملات المصدر. نوع عناصر الصفيف هو absl::AnyInvocable<SourceOperandInterface *>()>.
  • dest_op_getters_ صفيف لتخزين الوظائف القابلة للاستدعاء المستخدَمة للحصول على عناصر عامل التشغيل الوجهة نوع عناصر الصفيف هو absl::AnyInvocable<DestinationOperandInterface *>()>.
  • xreg_alias مصفوفة من عدد صحيح يسجّل أسماء ABI في RiscV، مثل "zero" و "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..

الدوال المساعِدة

سنبدأ بكتابة بعض الدوالّ المساعِدة التي نستخدمها لإنشاء عاملي حساب سجلّ المصدر والوجهة. سيتم إنشاء نماذج لهذه العناصر استنادًا إلى نوع السجلّ، وسيتم استدعاء عنصر 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);
}

كما ترون، هناك دالتان مساعدتان. يأخذ الإجراء الثاني مَعلمة إضافية 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;
}

دوالّ المُنشئ والواجهة

دالة الإنشاء والواجهة بسيطة جدًا. لا يفعل أسلوب الإنشاء سوى استدعاء الطريقتَين لإعداد صفائف الوظائف التي يمكن الاتصال بها لأجل مُستخدِمي المَعلمات.

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_);
}

أخيرًا، تُرجع وظائف جلب المَعلمات القيمة من دالة الجلب التي تستدعيها استنادًا إلى البحث في الصفيف باستخدام قيمة التعداد للمَعلمة الوجهة/المصدر.


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() أولاً، لأنّه لا يتوفّر سوى بضعة عوامل تشغيل للوجهة.

استرجع فئة 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. للتيسير، يتمّ إعداد كلّ إدخال باستخدام دالة لامدا، ولكن يمكنك استخدام أيّ شكل آخر من الدوالّ القابلة للاتّباع أيضًا. التوقيع لللامدا هو void(int latency).

حتى الآن، لم نتحدث كثيرًا عن الأنواع المختلفة من عاملي معالجة الوجهة المحدّدين في MPACT-Sim. في هذا التمرين، سنستخدم نوعين فقط من العناصر: generic::RegisterDestinationOperand المحدّد في register.h، وgeneric::DevNullOperand المحدّد في devnull_operand.h. إنّ تفاصيل هذه المَعلمات ليست مهمة حقًا في الوقت الحالي، باستثناء أنّه يتم استخدام الأولى للكتابة في السجلات، ويتجاهل الأخير كل عمليات الكتابة.

الإدخال الأول لـ 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". ويتم استخدامه كهدف لجميع تعليمات الفرع والقفز. تم تحديد الاسم في RiscVState على النحو التالي: kPcName.

  dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
  }

أخيرًا، هناك عامل التشغيل kRd الوجهة. في riscv32i.isa، لا يُستخدَم الم Operand rd إلا للإشارة إلى السجلّ الصحيح المشفَّر في حقل "rd" لكلمة التعليمات، وبالتالي لا يكون هناك أي غموض في ما يشير إليه. هناك تعقيد واحد فقط. سجل x0 (اسم آبي 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، يندرجون ضمن مجموعتَين. أحدهما هو عامل التشغيل المباشر: 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");
  };

يتم التعامل مع الم Operand kRs1 وkRs2 بشكل مماثل لـ kRd، باستثناء أنّه على الرغم من أنّنا لم نريد تعديل x0 (أو zero)، نريد التأكّد من أنّه يتم دائمًا قراءة 0 من هذا الم Operand. وسنستخدم لذلك فئة generic::IntLiteralOperand<> المحدّدة في literal_operand.h. يُستخدَم هذا المُشغِّل لتخزين قيمة حرفية (بدلاً من قيمة فورية مُحاكاة). بخلاف ذلك، يكون النمط متطابقًا: أولاً، استخرِج قيمة rs1/rs2 من كلمة التعليمات، وإذا كانت القيمة صفرًا، أعِد العنصر المرجعي باستخدام مَعلمة النموذج 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. الفرق الوحيد بين الوظائف المختلفة للحصول على المُشغِّلات الفورية هو دالة "المستخرج" المستخدَمة وما إذا كان نوع التخزين موقَّعًا أم بدون توقيع، وفقًا لـ bitfield.

  // 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_));
  };
}

إذا كنت بحاجة إلى مساعدة (أو أردت التحقّق من عملك)، يمكنك الاطّلاع على الإجابة الكاملة هنا.

وبهذا نصل إلى نهاية هذا الدليل التعليمي. نأمل أن تكون هذه المعلومات مفيدة.