جهاز فك الترميز المدمج 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 للاسم المحدّد في "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_;

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

تحديد طرق الفئات في برنامج فك الترميز

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

التالي هو إدخال تعداد المعامل Predicate وSource وDestination (الوجهة)، يحدد المعامل الذي يجب إنشاؤه. هذه تأتي من الثلاثة 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;

لتبسيط تنفيذ كل طريقة من طرق الحصول على المعامل سننشئ صفيفَتين من العناصر القابلة للاستدعاء (كائنات الدوال) مفهرَسة بواسطة القيمة الرقمية لأعضاء 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، مثل "صفر" أو "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_);
}

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


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

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

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

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