מפענח משולב 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

תוכלו לראות את ארבעת קובצי המקור שמכילים את קוד C++ שנוצר:

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

פותחים את הקובץ הראשון riscv32i_decoder.h. יש שלוש מחלקות לבדוק את:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

שימו לב לשמות של הכיתות. השמות של כל הכיתות מבוססים על גרסת אותיות רישיות של השם שניתן ב-"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_;
};

אין שיטות וירטואליות בכיתה הזו, לכן מדובר בכיתה עצמאית, להבחין בשני דברים. ראשית, ה-constructor לוקח את הסמן למופע של כיתה אחת (RiscV32IInstructionSetFactory). זו מחלקה שנוצרה המפענח משתמש בו כדי ליצור מופע של המחלקה RiscV32Slot, לפענח את כל ההוראות שהוגדרו עבור slot RiscV32 כפי שהן מוגדרות קובץ riscv32i.isa. שנית, השיטה Decode מקבלת פרמטר נוסף שמצביע לסוג RiscV32IEncodingBase, זו מחלקה שתספק את בין המקודד-מפענח שנוצר במדריך הראשון לבין הקובץ הבינארי שנוצר בשיעור ה-Lab השני.

הכיתה RiscV32IInstructionSetFactory היא כיתת מופשטת שממנה אנחנו שהם צריכים להסיק את ההטמעה שלנו למפענח המלא. ברוב המקרים, הכיתה היא טריוויאלית: פשוט מספקים שיטה לקריאה ל-constructor של כל אחד מחלקה של יחידת קיבולת (Slot) המוגדרת בקובץ .isa שלנו. במקרה שלנו, מאוד פשוט הוא רק מחלקה אחת כזו: Riscv32Slot (פסקל של השם riscv32 משורשרים ל-Slot). השיטה לא נוצרה עבורך כי יש מספר תרחישים מתקדמים לדוגמה שבהם ייתכן שיש תועלת בגזירה של מחלקה משנית מהמשבצת, ולקרוא ל-constructor שלה במקום זאת.

נעבור על השיעור האחרון 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);
}

אם אתם זקוקים לעזרה (או רוצים לבדוק את העבודה שלכם), התשובה המלאה היא כאן.

הגדרת המחלקה של המפענח

הצהרות לגבי מבנים, יצרנים ו-methods

בשלב הבא צריך להגדיר את מחלקת המפענח. באותו קובץ שצוין למעלה, עוברים אל ההצהרה RiscV32Decoder. הרחבת ההצהרה להגדרת מחלקה שבהן RiscV32Decoder יורשת מהיחידה הארגונית generic::DecoderInterface.

class RiscV32Decoder : public generic::DecoderInterface {
  public:
};

בשלב הבא, לפני שנכתוב את ה-constructor, בואו נסתכל על הקוד שנוצר במדריך השני שלנו על המפענח הבינארי. בנוסף לכל Extract פונקציות. יש את הפונקציה DecodeRiscVInst32:

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

הפונקציה הזו לוקחת את מילת ההוראה שצריך לפענח, ומחזירה את קוד ה-opcode שתואם להוראה הזו. לעומת זאת, מחלקה DecodeInterface ש-RiscV32Decoder מטמיעה רק מסירות address. לכן, לכיתה 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. כלומר, אנחנו צריכים להצהיר על ה-constructor כדי יכול לקחת מצביע אל state ואל memory:

RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);

מוחקים את constructor שמוגדר כברירת מחדל ומבטלים את כלי ההסבה:

RiscV32Decoder() = delete;
~RiscV32Decoder() override;

בשלב הבא צריך להצהיר על השיטה DecodeInstruction שצריך לבטל מ: generic::DecoderInterface.

generic::Instruction *DecodeInstruction(uint64_t address) override;

אם אתם זקוקים לעזרה (או רוצים לבדוק את העבודה שלכם), התשובה המלאה היא כאן.


ההגדרות של החברים בנתונים

בכיתה RiscV32Decoder צריכים להיות חברים עם מידע פרטי כדי לאחסן את את הפרמטרים של constructor ומצביע לסיווג היצרן.

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

אם אתם זקוקים לעזרה (או רוצים לבדוק את העבודה שלכם), התשובה המלאה היא כאן.

הגדרת ה-methods של קטגוריית המפענח

בשלב הבא הגיע הזמן להטמיע את ה-constructor, להרוס אמצעי תשלום אחד (DecodeInstruction). פותחים את הקובץ riscv32_decoder.cc. הריק כבר קיימות בקובץ וגם בהצהרות של מרחב שמות, מתוך using הצהרות.

הגדרת הבונה

ה-constructor צריך רק לאתחל את רכיבי הנתונים. קודם מאתחלים את 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);

הגדרת כלי נשק

ההרכבה הוא פשוט, פשוט משחררים את האובייקטים שהקצינו ב-constructor, אבל עם סיבוב אחד. המופע של מאגר הנתונים הזמני נספר כהפניה, כך שבמקום זאת כשקוראים לפונקציה 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 של ההוראה, האופרנדים של המקור והיעד שלו אופרנדים במשאבים. כל האובייקטים האלה תלויים במידע מהקובץ הבינארי למקודד-מפענח, כגון opcode, ערכים של שדות ספציפיים מילת הוראה וכו'. היא מופרדת ממחלקת המפענח כדי לשמור אותה. agnostic לקידוד ומאפשר תמיכה עבור סכימות קידוד שונות הוא בעתיד.

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

ה-method GetOpcode מחזירה את האיבר OpcodeEnum של של ההוראה, לזיהוי קוד ההפעלה של ההוראה. הכיתה OpcodeEnum היא מוגדר בקובץ ה-ISa-מפענח riscv32i_enums.h שנוצר. השיטה לוקחת שני פרמטרים, שאפשר להתעלם משניהם למטרות שלנו. הראשון מתוך זהו סוג יחידת הקיבולת (סוג enum גם שמוגדר ב-riscv32i_enums.h), מאחר של-RiscV יש רק משבצת אחת, יש לו רק ערך אפשרי אחד: SlotEnum::kRiscv32. השני הוא מספר המכונה של יחידת הקיבולת (במקרה יש כמה מופעים של יחידת הקיבולת (Slot), מה שעשוי להתרחש בחלק מ-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)' ייחודי קובע את סוג האופרנד המדויק שיוחזר, ואין צורך לוקחים בחשבון את הערכים של הפרמטרים משבצת, רשומה או 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:

};

בשלב הבא, ה-constructor צריך להעביר את הסמן למופע של המצב (State), מצביע אל riscv::RiscVState. צריך להשתמש ב-Destructor המוגדר כברירת מחדל.

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, בהתאמה. כך, הגופים של שיטות אלה מצומצמים לביצוע קריאה אובייקט פונקציה של ערך enum שמועבר ומחזיר את הערך שלו עם ערך מסוים.

כדי לארגן את האתחול של שני המערכים האלה, אנחנו מגדירים שיטות שייקראו מה-constructor כך:

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

חברים בנתונים

אלה הנתונים הנדרשים:

  • state_ כדי לשמור על הערך riscv::RiscVState *.
  • inst_word_ מסוג uint32_t שכולל את הערך של מילת הוראות.
  • opcode_ כדי להחזיק את קוד ה-opcode של ההוראה הנוכחית שמתעדכנת על ידי באמצעות ה-method 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 כדי לקבל כינוי רושמים אובייקט, ואז קוראים ל-method של מפעל אופרנד באובייקט הרישום.

נתחיל עם העוזרי אופרנד היעד:

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

פונקציות build ופונקציות ממשק

הפונקציות של ה-constructor והממשק הם מאוד פשוטות. ה-constructor קוראת לשתי שיטות האתחול לאתחל את מערכי הקריאה מקבלי האופרנד.

RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
  InitializeSourceOperandGetters();
  InitializeDestinationOperandGetters();
}

ParseInstruction שומר את מילת ההוראה ואז את ה-opcode מקבל מקריאה לקוד שנוצר למפענח הבינארי.

// 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 שהיא מפעילה. על סמך חיפוש המערך באמצעות ערך אופרנד של יעד/מקור.


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, והוא מתייחס ל'מחשב'. אומנם, הוא משמש כיעד את כל ההוראות להסתעפות ולקפיצה. השם מוגדר ב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]);
    )
  }
}

עכשיו ל-method 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, אחרת מוחזר רישום רגיל אופרנד מקור באמצעות פונקציית העזרה, תוך שימוש בכינוי 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_));
  };
}

אם אתם זקוקים לעזרה (או רוצים לבדוק את העבודה שלכם), התשובה המלאה היא כאן.

סיימנו את המדריך הזה. אנחנו מקווים שהצלחנו לעזור לך.