رسیور مجتمع 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_;

اگر به کمک نیاز دارید (یا می خواهید کار خود را بررسی کنید)، پاسخ کامل اینجاست .

متدهای کلاس رمزگشا را تعریف کنید

در مرحله بعد، نوبت به پیاده سازی سازنده، تخریب کننده و متد 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);

تعریف ویرانگر

Destructor ساده است، فقط اشیایی را که در سازنده اختصاص داده‌ایم آزاد کنید، اما با یک چرخش. نمونه بافر داده مرجع شمارش می شود، بنابراین به جای فراخوانی 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 را فراخوانی می کنیم که آدرس و کلاس Encoding را ارسال می کند.

  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 تعریف شده است. این روش دو پارامتر دارد که هر دو را می توان برای اهداف ما نادیده گرفت. اولین مورد از این نوع اسلات (یک کلاس 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);

دو روش بعدی برای مدلسازی منابع سخت افزاری در پردازنده به منظور بهبود دقت چرخه استفاده می شود. برای تمرین‌های آموزشی خود، از اینها استفاده نمی‌کنیم، بنابراین در پیاده‌سازی، آن‌ها حذف می‌شوند و 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 صدق نمی کند.

بعد محمول، منبع، و مقصد، ورودی شمارش عملوند است که عملوندی را که باید ایجاد شود، مشخص می کند. اینها از سه 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 به‌طور منحصربه‌فرد نوع دقیق عملوند را برای بازگشت تعیین می‌کند و نیازی به در نظر گرفتن مقادیر اسلات، ورودی نیست. یا پارامترهای اپکد

در نهایت، برای عملوندهای مبدا و مقصد، موقعیت ترتیبی عملوند وارد می شود (دوباره می توانیم این را نادیده بگیریم)، ​​و برای عملوند مقصد، تأخیر (در چرخه) که بین زمان صدور دستورالعمل و نتیجه مقصد برای دستورالعمل های بعدی در دسترس است. در شبیه ساز ما، این تأخیر 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; }

در نهایت روش باقیمانده را اضافه کنید.


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 نمایه می شوند. به این ترتیب بدنه‌های این متدها به فراخوانی شی تابع برای مقدار enum که ارسال می‌شود و مقدار بازگشتی آن کاهش می‌یابد.

برای سازماندهی اولیه سازی این دو آرایه، دو روش خصوصی تعریف می کنیم که از سازنده به صورت زیر فراخوانی می شوند:

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

اعضای داده

اعضای داده مورد نیاز به شرح زیر است:

  • state_ برای نگه داشتن مقدار riscv::RiscVState * .
  • inst_word_ از نوع uint32_t که مقدار کلمه دستور فعلی را نگه می دارد.
  • opcode_ برای نگه داشتن 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 که بر اساس جستجوی آرایه با استفاده از مقدار 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)]();
}

روش های اولیه سازی آرایه

همانطور که ممکن است حدس زده باشید، بیشتر کار روی مقداردهی اولیه آرایه های گیرنده است، اما نگران نباشید، این کار با استفاده از یک الگوی آسان و تکراری انجام می شود. بیایید ابتدا با 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 عملوند rd فقط برای ارجاع به رجیستر عدد صحیح کدگذاری شده در فیلد "rd" کلمه دستورالعمل استفاده می شود، بنابراین هیچ ابهامی وجود ندارد که به آن اشاره دارد. فقط یک عارضه وجود دارد. ثبات x0 (نام abi zero ) به 0 متصل شده است، بنابراین برای آن ثبات از DevNullOperand استفاده می کنیم.

بنابراین در این گیرنده ابتدا مقدار فیلد 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");
  };

عملوندهای kRs1 و kRs2 معادل kRd هستند، با این تفاوت که در حالی که ما نمی‌خواستیم x0 (یا zero ) را به‌روزرسانی کنیم، می‌خواهیم مطمئن شویم که همیشه 0 را از آن عملوند می‌خوانیم. برای آن ما از کلاس generic::IntLiteralOperand<> تعریف شده در literal_operand.h استفاده می کنیم. این عملوند برای ذخیره یک مقدار تحت اللفظی (بر خلاف یک مقدار فوری شبیه سازی شده) استفاده می شود. در غیر این صورت الگوی مشابه است: ابتدا مقدار rs1/rs2 را از کلمه دستورالعمل استخراج کنید، اگر صفر بود عملوند تحت اللفظی را با پارامتر قالب 0 برگردانید، در غیر این صورت یک عملوند منبع ثبات معمولی را با استفاده از تابع helper، با استفاده از نام مستعار 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 ذخیره می شوند. تنها تفاوت بین دریافت‌کننده‌های مختلف برای عملوندهای فوری این است که از کدام تابع Extractor استفاده می‌شود و اینکه نوع ذخیره‌سازی با توجه به فیلد بیتی علامت‌دار یا بدون علامت است.

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

اگر به کمک نیاز دارید (یا می خواهید کار خود را بررسی کنید)، پاسخ کامل اینجاست .

با این کار این آموزش به پایان می رسد. امیدواریم مفید بوده باشد.