מפענח ISA של RiscV

מטרות המדריך הזה הן:

  • הסבר על האופן שבו ההוראות מיוצגות בסימולטור MPACT-Sim.
  • הסבר על המבנה והתחביר של קובץ התיאור של ISA
  • צריך לכתוב את תיאורי ה-ISA עבור קבוצת המשנה RiscV RV32I של ההוראות

סקירה כללית

ב-MPACT-Sim, הוראות היעד מפוענחות ומאוחסנות כדי להפוך את המידע שלהם לזמין יותר, ואת הסמנטיקה מהיר יותר לביצוע. מכונות ההוראות האלה נשמרות במטמון לשמור במטמון כדי לצמצם את מספר הפעמים שהוראות שמבוצעות בתדירות גבוהה בוצעה.

שיעור ההוראה

לפני שנתחיל, כדאי לבחון קצת את ההוראות שמיוצגים ב-MPACT-Sim. המחלקה Instruction מוגדרת במקומות mpact-sim/mpact/sim/generic/instruction.h.

המכונה של מחלקת ההוראה מכילה את כל המידע שצריך כדי: מדמים את ההוראה לאחר הביצוע שלה, למשל:

  1. כתובת להוראות, סימולציה של גודל הוראה, כלומר הגודל בסיומת טקסט.
  2. קוד opcode של הוראות.
  3. מצביע פרדיקט של ממשק האופרנד (אם רלוונטי).
  4. וקטור של מצביע על ממשק אופרנד מקור.
  5. וקטור של מצביעי ממשק אופרנד יעד.
  6. אפשר לקרוא לפונקציה סמנטית.
  7. מצביעים על אובייקט של מצב ארכיטקטוני.
  8. מצביעים על אובייקט הקשר.
  9. מצביעים על הילד או הילדה ועל המופעים של ההוראות הבאות.
  10. מחרוזת פירוק.

המכונות האלה בדרך כלל נשמרות במטמון של הוראות (מכונה). בשימוש חוזר בכל פעם שהוראה מבוצעת מחדש. זה משפר את הביצועים במהלך זמן הריצה.

מלבד המצביע לאובייקט ההקשר, כל הערכים ימולאו על ידי שנוצר מהתיאור של ה-ISA. בשביל זה אין צורך לדעת פרטים על הפריטים האלה, באמצעותם ישירות. במקום זאת, יש הבנה כללית של אופן השימוש בהם מספיק.

הפונקציה הסמנטית שאפשר לקרוא לה היא אובייקט הפונקציה/ה-method/הפונקציה C++ (כולל lambdas) לפי הסמנטיקה של ההוראה. עבור להוראה add, הוא טוען כל אופרנד מקור, הפונקציה מוסיפה אופרנד וכותב את התוצאה לאופרנד יעד יחיד. הנושא של פונקציות סמנטיות מפורטות בהרחבה במדריך בנושא פונקציות סמנטיקה.

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

שיעור ההוראה כולל סימונים לשלושה סוגים של ממשקי אופרנד: פרדיקט, מקור ויעד. הממשקים האלה מאפשרים לפונקציות סמנטיות להיות כתובות בנפרד מהסוג בפועל של ההוראה הבסיסית באופרנד. למשל, ניתן לגשת לערכים של רישומים ושל פניות מיידיות דרך אותו ממשק. כלומר, הוראות שמבצעות את אותן פעולות אבל באופרנדים שונים (למשל, רישומים לעומת ימים). הטמענו באמצעות אותה פונקציה סמנטית.

ממשק האופרנד הפרדיקט, עבור ISA שתומכים בחיזוי ביצוע הוראה (ב-ISAs אחרים הוא null), משמש כדי לקבוע אם ההוראה הנתונה צריכה להתבצע על סמך הערך הבוליאני של הפרדיקט.

// The predicte operand interface is intended primarily as the interface to
// read the value of instruction predicates. It is separated from source
// predicates to avoid mixing it in with the source operands needed for modeling
// the instruction semantics.
class PredicateOperandInterface {
 public:
  virtual bool Value() = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
  virtual ~PredicateOperandInterface() = default;
};

ממשק האופרנד של המקור מאפשר לפונקציה הסמנטית של ההוראה לקרוא מהאופרנדים של ההוראות, ללא קשר לאופרנד הבסיסי מהסוג הזה. שיטות הממשק תומכות גם באופרנדים סקלריים וגם בעלי ערך וקטורי.

// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
 public:
  // Methods for accessing the nth value element.
  virtual bool AsBool(int index) = 0;
  virtual int8_t AsInt8(int index) = 0;
  virtual uint8_t AsUint8(int index) = 0;
  virtual int16_t AsInt16(int index) = 0;
  virtual uint16_t AsUint16(int) = 0;
  virtual int32_t AsInt32(int index) = 0;
  virtual uint32_t AsUint32(int index) = 0;
  virtual int64_t AsInt64(int index) = 0;
  virtual uint64_t AsUint64(int index) = 0;

  // Return a pointer to the object instance that implements the state in
  // question (or nullptr) if no such object "makes sense". This is used if
  // the object requires additional manipulation - such as a fifo that needs
  // to be pop'ed. If no such manipulation is required, nullptr should be
  // returned.
  virtual std::any GetObject() const = 0;

  // Return the shape of the operand (the number of elements in each dimension).
  // For instance {1} indicates a scalar quantity, whereas {128} indicates an
  // 128 element vector quantity.
  virtual std::vector<int> shape() const = 0;

  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;

  virtual ~SourceOperandInterface() = default;
};

ממשק אופרנד היעד מספק שיטות להקצאה ולטיפול DataBuffer מכונות (סוג הנתונים הפנימי שמשמש לשמירת ערכים). א' גם לאופרנד יעד משויך זמן אחזור, כלומר מחזורים להמתין עד למופע של מאגר הנתונים הזמני שהוקצתה ההוראה פונקציה סמנטית משמשת לעדכון הערך של רשם היעד. עבור למשל, זמן האחזור של הוראה מסוג add יכול להיות 1, ואילו זמן האחזור של mpy יכולה להיות 4. כל זה מוסבר בפירוט של פונקציות סמנטיות.

// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
 public:
  virtual ~DestinationOperandInterface() = default;
  // Allocates a data buffer with ownership, latency and delay line set up.
  virtual DataBuffer *AllocateDataBuffer() = 0;
  // Takes an existing data buffer, and initializes it for the destination
  // as if AllocateDataBuffer had been called.
  virtual void InitializeDataBuffer(DataBuffer *db) = 0;
  // Allocates and initializes data buffer as if AllocateDataBuffer had been
  // called, but also copies in the value from the current value of the
  // destination.
  virtual DataBuffer *CopyDataBuffer() = 0;
  // Returns the latency associated with the destination operand.
  virtual int latency() const = 0;
  // Return a pointer to the object instance that implmements the state in
  // question (or nullptr if no such object "makes sense").
  virtual std::any GetObject() const = 0;
  // Returns the order of the destination operand (size in each dimension).
  virtual std::vector<int> shape() const = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
};

תיאור ISA

ה-ISA (ארכיטקטורת קבוצת הוראות) של מעבד מגדיר את המודל המופשט שבהם התוכנה מקיימת אינטראקציה עם החומרה. הוא מגדיר את הקבוצה הוראות זמינות, את סוגי הנתונים, הרישום ומצבי מכונה אחרים פועלים לפיהן, וגם את ההתנהגות שלהן (סמנטיקה). למטרות של MPACT-Sim, ה-ISA לא כולל את הקידוד בפועל של ההוראות. אנחנו מטפלים בזה בנפרד.

ה-ISA של המעבד מצוין בקובץ תיאור שמתאר את ברמת קידוד מופשטת ואגנוסטית. קובץ התיאורים מספור את קבוצת ההוראות הזמינות. לכל הוראה לציון שם, מספר ושמות של אופרנדים, קישור לפונקציה C++ או קריאה שמטמיעה את הסמנטיקה שלה. בנוסף, אפשר לציין מחרוזת עיצוב לפירוק, והשימוש בהוראה שמות של משאבי חומרה. האפשרות הראשונה היא שימושית ליצירת טקסט ייצוג של ההוראה לניפוי באגים, מעקב או שימוש אינטראקטיבי. כדי לבנות מודלים מדויקים יותר של מחזוריות בסימולציה.

קובץ התיאור של ה-ISA מנותח על ידי ה-isa-parser, שיוצר קוד עבור למפענח ההוראה והייצוג. המפענח הזה אחראי אכלוס השדות של אובייקטי ההוראה. הערכים הספציפיים, מספר רישום היעד, מתקבלים מהוראה ספציפית לפורמט וממפענח. אחד המפענחים האלה הוא המפענח הבינארי, שמתמקד במדריך הבא.

מדריך זה מסביר איך לכתוב קובץ תיאור ISA של קובץ סקלר פשוט של הארכיטקטורה, נשתמש בקבוצת משנה של הוראות RiscV RV32I שהוגדרו נמחיש זאת, וביחד עם מדריכים אחרים, בונים סימולטור הדמייה של "Hello World" בתוכנית. לפרטים נוספים על ה-RiscV ISA, ראו מפרטי Risc-V.

לשם כך, פותחים את הקובץ: riscv_isa_decoder/riscv32i.isa

התוכן של הקובץ מחולק למספר קטעים. הראשון הוא ה-ISA הצהרה:

isa RiscV32I {
  namespace mpact::sim::codelab;
  slots { riscv32; }
}

זה מצהיר ש-RiscV32I הוא השם של ה-ISA ומחולל הקוד ליצור מחלקה בשם RiscV32IEncodingBase שמגדירה את הממשק שיצר המפענח ישתמש כדי לקבל מידע opcode ואופרנד. השם של המחלקה נוצרת על ידי המרת שם ה-ISA לפסקל, ואז משרשרת את זה עם EncodingBase. ההצהרה slots { riscv32; } מציין שיש רק משבצת הוראה אחת riscv32 ב-RiscV32I ISA (בניגוד למספר יחידות קיבולת בהוראה של VLIW), הוראות תקפות הן אלה שמוגדרות לבצע ב-riscv32.

// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};

מציין שהחלק הראשון של פירוק בתהליך פירוק כלשהו המפרט (ראו מידע נוסף בהמשך), יופיע בצד ימין ב-15 תווים שדה רחב. כל המקטעים הבאים יצורפו לשדה הזה בלי כל ריווח נוסף.

מתחתיה יש שלוש הצהרות מיקומי מודעות: riscv32i, zicsr ו-riscv32. על סמך ההגדרה של isa שלמעלה, רק הוראות שהוגדרו עבור riscv32 משבצת הזמן תהיה חלק מה-isa של RiscV32I. למה מיועדות שתי המשבצות האחרות?

ניתן להשתמש במשבצות כדי לחלק את ההוראות לקבוצות נפרדות, ואז משולבות לחריץ אחד בסוף. שימו לב לסימון : riscv32i, zicsr בהצהרה של יחידת הקיבולת (Slot) riscv32. ההגדרה הזו מציינת שהמשבצת riscv32 מקבלת בירושה את כל ההוראות המוגדרות במשבצות zicsr ו-riscv32i. ה-ISA של RiscV 32 סיביות מורכב מ-ISA בסיסי בשם RV32I, שאליו קבוצה של תוספים אופציונליים להוסיף. מנגנון מיקומי המודעות מאפשר להוראות בתוספים האלה ולציין בנפרד ואז משולבות לפי הצורך כדי להגדיר את ל-ISA באופן כללי. במקרה זה, הוראות במקטע RiscV 'I' קבוצות מוגדרות בנפרד מאלה שב-zicsr קבוצה. אפשר להגדיר קבוצות נוספות ל-'M' (כפל/חילוק), 'F' (נקודה צפה בדיוק חד-פעמי), 'D' (נקודה צפה עם דיוק כפול), 'C' (הוראות קומפקטיות של 16 סיביות) וכו' הדרושים ל-RiscV ISA הסופי שרוצים.

// The RiscV 'I' instructions.
slot riscv32i {
  ...
}

// RiscV32 CSR manipulation instructions.
slot zicsr {
  ...
}

// The final instruction set combines riscv32i and zicsr.
slot riscv32 : riscv32i, zicsr {
  ...
}

אין צורך לשנות את הגדרות מיקומי המודעות zicsr ו-riscv32. אבל מתמקדים במדריך הזה להוסיף את ההגדרות הנדרשות אל riscv32i משבצת זמן אחת. בואו נבחן מקרוב את מה שמוגדר כרגע במיקום הזה:

// The RiscV 'I' instructions.
slot riscv32i {
  // Include file that contains the declarations of the semantic functions for
  // the 'I' instructions.
  includes {
    #include "learning/brain/research/mpact/sim/codelab/riscv_semantic_functions/solution/rv32i_instructions.h"
  }
  // These are all 32 bit instructions, so set default size to 4.
  default size = 4;
  // Model these with 0 latency to avoid buffering the result. Since RiscV
  // instructions have sequential semantics this is fine.
  default latency = 0;
  // The opcodes.
  opcodes {
    fence{: imm12 : },
      semfunc: "&RV32IFence"c
      disasm: "fence";
    ebreak{},
      semfunc: "&RV32IEbreak",
      disasm: "ebreak";
  }
}

קודם כול, יש קטע includes {} שבו מפורטים קובצי הכותרות שצריך להיכלל בקוד שנוצר כאשר יש הפניה לחריץ הזה, באופן ישיר או בעקיפין, ב-ISA הסופי. אפשר לכלול קבצים גם ברשימת הקבצים ברחבי העולם קטע includes {} בהיקף, במקרה כזה הם תמיד נכללים. מי יכול שימושי אם אחרת היה צריך להוסיף את אותו קובץ הכללה לכל מיקום להגדרה.

ההצהרות default size ו-default latency מגדירות זאת, אלא אם והגודל של הוראה אחרת הוא 4, ושזמן האחזור כתיבת אופרנד היעד היא 0 מחזורים. חשוב לשים לב, גודל ההוראה שצוין כאן, הוא הגודל של התוספת למונה של התוכנית כדי לחשב את של ההוראה הרציפה הבאה שצריך לבצע ומעבד מידע. הוא עשוי להיות זהה לגודל בבייטים של ייצוג של הוראה בקובץ ההפעלה של הקלט.

קטע הקוד של הקוד הוא המרכזי בהגדרת יחידת הקיבולת. כמו שאפשר לראות, קודי opcode (הוראות) fence ו-ebreak הוגדרו עד עכשיו ב- riscv32i. קוד ה-opcode של fence מוגדר על ידי ציון השם (fence) וגם מפרט האופרנד ({: imm12 : }), ואחריו אפשר לבצע פירוק הפורמט ("fence") והקריאה שניתן להתקשר אליו באמצעות הנוסחה הסמנטית ("&RV32IFence").

האופרנדים להוראה מוגדרים כמשולשים, כאשר כל רכיב ומפרידים ביניהן באמצעות נקודה-פסיק, חיזוי ':' source operand list ':' רשימת אופרנדים של יעדים. רשימות האופרנד של המקור והיעד מסומנות בפסיקים רשימות מופרדות של שמות אופרנדים. כמו שאפשר לראות, ההוראות פועלות ההוראה fence מכילה, אין אופרנדים פרדיקטיים, אלא רק מקור אחד שם אופרנד imm12, ואין אופרנדים ליעד. קבוצת המשנה RiscV RV32I לא יתמוך בביצוע חזוי, כך שהאופרנד של הפרדיקט תמיד יהיה ריק במדריך הזה.

הפונקציה הסמנטית מצוינת כמחרוזת הדרושה כדי לציין את הפונקציה C++ או קריאה לפונקציה הסמנטית. החתימה של הפונקציה הסמנטית/הניתנת לקריאה היא void(Instruction *).

מפרט הפרוק מורכב מרשימת מחרוזות שמופרדות בפסיקים. בדרך כלל משתמשים רק בשתי מחרוזות, אחת ל-opcode ואחת בשביל אופרנדים. כשהן מעוצבות (באמצעות הקריאה ל-AsString() בהוראה), כל אחת המחרוזת מעוצבת בתוך שדה בהתאם לdisasm widths המפרט שתואר קודם לכן.

התרגילים הבאים יעזרו לך להוסיף הוראות לקובץ riscv32i.isa מספיק כדי לדמות 'שלום עולם' בתוכנית. למי שממהר, בפתרונות riscv32i.isa וגם rv32i_instructions.h


ביצוע פיתוח ראשוני

אם לא שינית את הספרייה ל-riscv_isa_decoder, עליך לעשות זאת עכשיו. לאחר מכן וליצור את הפרויקט באופן הבא – ה-build הזה אמור להצליח.

$ cd riscv_isa_decoder
$ bazel build :all

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

#ifndef RISCV32I_ENUMS_H
#define RISCV32I_ENUMS_H

namespace mpact {
namespace sim {
namespace codelab {
  enum class SlotEnum {
    kNone = 0,
    kRiscv32,
  };

  enum class PredOpEnum {
    kNone = 0,
    kPastMaxValue = 1,
  };

  enum class SourceOpEnum {
    kNone = 0,
    kCsr = 1,
    kImm12 = 2,
    kRs1 = 3,
    kPastMaxValue = 4,
  };

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kRd = 2,
    kPastMaxValue = 3,
  };

  enum class OpcodeEnum {
    kNone = 0,
    kCsrs = 1,
    kCsrsNw = 2,
    kCsrwNr = 3,
    kEbreak = 4,
    kFence = 5,
    kPastMaxValue = 6
  };

  constexpr char kNoneName[] = "none";
  constexpr char kCsrsName[] = "Csrs";
  constexpr char kCsrsNwName[] = "CsrsNw";
  constexpr char kCsrwNrName[] = "CsrwNr";
  constexpr char kEbreakName[] = "Ebreak";
  constexpr char kFenceName[] = "Fence";
  extern const char *kOpcodeNames[static_cast<int>(
      OpcodeEnum::kPastMaxValue)];

  enum class SimpleResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class ComplexResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class AttributeEnum {
    kPastMaxValue = 0
  };

}  // namespace codelab
}  // namespace sim
}  // namespace mpact

#endif  // RISCV32I_ENUMS_H

כמו שאפשר לראות, כל משבצת, קוד פעולה ואופרנד שהוגדרו קובץ riscv32i.isa מוגדר באחד מסוגי הספירה. בנוסף יש מערך OpcodeNames שמאחסן את כל שמות ה-opcodes מוגדרת ב-riscv32i_enums.cc). הקבצים האחרים מכילים את המפענח שנוצר, שנעסוק בו יותר במדריך אחר.

כלל בניית בייזל

יעד המפענח של ISA ב-Bazel מוגדר באמצעות מאקרו של כלל מותאם אישית שנקרא mpact_isa_decoder, שנטען מ-mpact/sim/decoder/mpact_sim_isa.bzl במאגר mpact-sim. במדריך הזה, יעד ה-build שהוגדר riscv_isa_decoder/BUILD הוא:

mpact_isa_decoder(
    name = "riscv32i_isa",
    src = "riscv32i.isa",
    includes = [],
    isa_name = "RiscV32I",
    deps = [
        "//riscv_semantic_functions:riscv32i",
    ],
)

הכלל הזה מפעיל את המחולל והכלי לניתוח ISA כדי ליצור את קוד C++ מה שהופך את התוכן שנוצר לספרייה שכללים אחרים יכולים להסתמך עליה התווית //riscv_isa_decoder:riscv32i_isa. הקטע includes נמצא בשימוש כדי לציין קובצי .isa נוספים שקובץ המקור עשוי לכלול. isa_name משמש לציון סוג ה-ISA הספציפי. הוא נדרש אם יש יותר בקובץ המקור שעבורו תיווצר המפענח.


הוספת הוראות לרישום-רישום ALU

עכשיו הגיע הזמן להוסיף כמה הוראות חדשות לקובץ riscv32i.isa. הראשון קבוצת הוראות הן הוראות ALU לרישום-רישום, כמו add, and, וכו' ב-RiscV32, כל השיטות האלה משתמשות בפורמט הוראה בינארית מסוג R:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 rs2 rs1 func3 שלישי קוד פעולה

אמנם הקובץ .isa משמש ליצירת מפענח מפענח בפורמט חדש, אבל הוא עדיין שימושי כדי לקחת בחשבון את הפורמט הבינארי ואת הפריסה שלו, כדי להנחות את הערכים. לפי המיקום שלך שאפשר לראות, יש שלושה שדות שרלוונטיים למפענח שמאכלס את האובייקטים של ההוראה: rs2, rs1 ו-rd. בשלב הזה בוחרים להשתמש את השמות האלה למספרים שלמים שמקודדים באותו אופן (רצפי ביט), באותם שדות הוראה, בכל ההוראות.

ההוראות שנוסיף הן:

  • add – הוספת מספר שלם.
  • and - ברמת הסיביות ו-.
  • or – ברמת הסיביות או,
  • sll – הזזת לוגיקה שמאלה.
  • sltu - הגדרה של 'פחות מ-', ללא חתימה.
  • sub – החסרה של מספרים שלמים.
  • xor – xor ברמת הסיביות.

כל אחת מההוראות האלה תתווסף לקטע opcodes של הגדרת משבצת riscv32i. נזכיר שאנחנו צריכים לציין את השם, את הקוד, של פירוק ושל הפונקציה הסמנטית לכל הוראה. השם קל, נשתמש פשוט בשמות ה-opcode שלמעלה. בנוסף, בכולם משתמשים באותם אופרנדים, כך אנחנו יכולים להשתמש ב-{ : rs1, rs2 : rd} למפרט האופרנד. המשמעות היא באופרנד של מקור הרישום שצוין ב-rs1 יהיה אינדקס 0 במקור וקטור אופרנד באובייקט ההוראה, אופרנד מקור רישום שצוין ב-rs2 יהיה אינדקס 1, ואופרנד היעד הרשום יצוין על ידי rd יהיה הרכיב היחיד בווקטור אופרנד היעד (באינדקס 0).

השלב הבא הוא מפרט הפונקציה הסמנטית. הפעולה הזו מתבצעת באמצעות מילת המפתח semfunc ומחרוזת C++ שמציינת קריאה שניתן להשתמש בה כדי להקצות אל std::function. במדריך הזה נשתמש בפונקציות, המחרוזת תהיה "&MyFunctionName". שימוש בסכימת השמות שהוצעה על ידי ההוראה fence, הערכים צריכים להיות "&RV32IAdd", "&RV32IAnd" וכו'.

השלב האחרון הוא מפרט הפרוק. הוא מתחיל במילת המפתח disasm ו ואחריה מופיעה רשימת מחרוזות מופרדות בפסיקים שמציינת איך ההוראה צריכה להיות מודפסת כמחרוזת. שימוש בכניסה % לפני שם אופרנד מציין החלפה של מחרוזת באמצעות ייצוג המחרוזת של שאופרנד. בהוראה add, זה יהיה: disasm: "add", "%rd, %rs1,%rs2". כלומר, הרשומה של ההוראה add אמורה להיראות כמו:

    add{ : rs1, rs2 : rd},
      semfunc: "&RV32IAdd",
      disasm: "add", "%rd, %rs1, %rs2";

אפשר לערוך את הקובץ riscv32i.isa ולהוסיף את כל ההוראות האלה אל תיאור אחד (.isa). אם אתם זקוקים לעזרה (או שאתם רוצים לבדוק את העבודה שלכם), קובץ התיאור הוא כאן.

אחרי שמוסיפים את ההוראות לקובץ riscv32i.isa, יהיה צורך בכך להוסיף הצהרות על פונקציות לכל אחת מהפונקציות הסמנטיות החדשות בוצעה הפניה לקובץ rv32i_instructions.h שנמצא ב- `../semantic_functions/. שוב, אם אתה צריך עזרה (או רוצה לבדוק את העבודה שלך), התשובה היא כאן.

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


הוספת הוראות ל-ALU עם מיידי

הקבוצה הבאה של ההוראות שנוסיף היא הוראות ALU שמבוססות על ערך מיידי במקום אחד מהרישומים. יש שלוש קבוצות כאלה הוראות (על סמך השדה המיידי): הוראות מיידיות מסוג I-Type עם קוד אימות מיידי של 12 ביט, ההוראות המיידיות מבוססות-I-Type למשילות, ו-U-Type מיידי, עם ערך מיידי לא חתום של 20 ביט. הפורמטים הם:

הפורמט המיידי I-Type:

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 שלישי קוד פעולה

הפורמט המיידי המיוחד I-Type:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 שלישי קוד פעולה

הפורמט המיידי מסוג U:

31..12 11..7 6..0
20 5 7
uimm20 שלישי קוד פעולה

כמו שאפשר לראות, שמות האופרנד rs1 ו-rd מתייחסים לאותם שדות ביטים כמו קודם, ומשמשים לייצוג מספרים שלמים, כך שהשמות עדיין פעילים. שדות הערך המיידי הם באורך ובמיקום שונים, וגם שתיים (uimm5 ו-uimm20) לא חתומות, ואילו imm12 חתום. כל אחד מ- הם ישתמשו בשם משלהם.

לכן האופרנדים של ההוראות מסוג I-Type צריכים להיות { : rs1, imm12 :rd }. עבור ההוראות המיוחדות מסוג I-Type, הערך צריך להיות { : rs1, uimm5 : rd}. מפרט האופרנד של הוראה מסוג U-Type צריך להיות { : uimm20 : rd }.

ההוראות מסוג I-Type שעלינו להוסיף הן:

  • addi – הוספה מיידית.
  • andi – סיביות ובאופן מיידי.
  • ori – סיביות או באופן מיידי.
  • xori – ערך Xor של כלשהו באופן מיידי.

ההוראות הספציפיות ל-I-Type שעלינו להוסיף הן:

  • slli – שינוי היגיון שמאלה באופן מיידי.
  • srai - שינוי שיטת החשבון הימנית באופן מיידי.
  • srli – שינוי הלוגיקה הימנית מייד.

ההוראות מסוג U שעלינו להוסיף הן:

  • auipc - הוספה של למעלה מיידית למחשב PC.
  • lui – טעינה מיידית למעלה.

השמות שבהם צריך להשתמש למקודדים מגיעים באופן טבעי משמות ההוראות למעלה (אין צורך להמציא הגדרות חדשות – כולן ייחודיות). כאשר מדובר שמציין את הפונקציות הסמנטיות, זכרו שאובייקטים ההוראות מקודדים ממשקים לאופרנדים של המקור שאינם תלויים באופרנד הבסיסי מהסוג הזה. כלומר, לגבי הוראות שמבצעים את אותה פעולה, עשויים להיות שונים בסוגי האופרנד, שיכולים לחלוק את אותה פונקציה סמנטית. לדוגמה, ההוראה addi מבצעת את אותה פעולה כמו ההוראה add, אחד מתעלם מסוג האופרנד, ולכן הוא יכול להשתמש באותה פונקציה סמנטית המפרט "&RV32IAdd". באופן דומה, גם עבור andi, ori, xori ו-slli. בהוראות האחרות נעשה שימוש בפונקציות סמנטיות חדשות, אבל צריך לתת להן שמות על סמך הפעולה, לא אופרנדים, לכן בשביל srai צריך להשתמש ב-"&RV32ISra". להוראות מסוג U, auipc ו-lui אין שווי ערך, אז זה בסדר כדי להשתמש ב-"&RV32IAuipc" וב-"&RV32ILui".

מחרוזות הפרוק דומות מאוד למחרוזות התרגיל הקודם, אבל כפי שהיית מצפה, הפניות אל %rs2 מוחלפות ב-%imm12, %uimm5 או %uimm20, לפי הצורך.

בצעו את השינויים כדי לשפר את התוצאות. בודקים את הפלט שנוצר. בדיוק כמו קודם לכן, אפשר לבדוק את העבודה שלכם riscv32i.isa את הרצף rv32i_instructions.h


הוראות ההסתעפות והקישור, שבהן אנחנו צריכים להוסיף, משתמשות ביעד אופרנד שמשתמע רק בהוראה עצמה, כלומר המחשב הבא עם ערך מסוים. בשלב הזה נתייחס לזה כאופרנד מתאים עם השם next_pc הוא יוגדר בפירוט בהמשך המדריך.

הוראות בנוגע להסתעפות

ההסתעפויות שאנחנו מוסיפים משתמשות בקידוד מסוג B-Type.

31 30..25 24..20 19..15 14..12 11..8 7 6..0
1 6 5 5 3 4 1 7
IM IM rs2 rs1 func3 IM IM קוד פעולה

השדות המיידיים השונים משורשרים לישות מיידית חתומה של 12 ביט עם ערך מסוים. מאחר שהפורמט לא רלוונטי באמת, נקרא עכשיו bimm12, להסתעפות של 12 ביט באופן מיידי. נתייחס למקטע הזה ב: במדריך הבא מוסבר איך ליצור את המפענח הבינארי. כל הסתעפות משווה בין פעולות המספרים השלמים שצוינו ב-rs1 וב-rs2, אם התנאי מתקיים, הערך המיידי מתווסף לערך הנוכחי של המחשב כדי תיצור את הכתובת של ההוראה הבאה שצריך לבצע. האופרנדים של לכן, הוראות ההסתעפות צריכות להיות { : rs1, rs2, bimm12 : next_pc }.

הוראות ההסתעפות שעלינו להוסיף הן:

  • beq – הסתעפות אם שווה.
  • bge – הסתעפות אם גדול מ- או שווה.
  • bgeu - הסתעפות אם היא גדולה או שווה ללא חתימה.
  • blt – הסתעפות אם פחות מ-.
  • bltu - הסתעפות אם פחות מאשר ללא חתימה.
  • bne – הסתעפות אם לא שווה ל-.

שמות ה-opcode האלה הם ייחודיים, כך שאפשר להשתמש בהם שוב ב-.isa לתיאור. כמובן שצריך להוסיף שמות חדשים של פונקציות סמנטיות, למשל, "&RV32IBeq" וכו'

כרגע, מפרט הפרוק קצת יותר מורכב, כי הכתובת של משמש לחישוב היעד, בלי שהוא יהיה חלק של האופרנדים להוראה. עם זאת, זהו חלק מהמידע שמאוחסן של אובייקט ההוראה, ולכן הוא זמין. הפתרון הוא להשתמש של הביטוי במחרוזת הפרוק. במקום להשתמש ב-'%' ואחריו את שם האופרנד אפשר להקליד %(expression: print format). פשוט מאוד יש תמיכה בביטויים, אבל אחד מהם הוא אחד מהם, כאשר המאפיין @ כולל קיזוז כתובת הסמל המשמש לכתובת הנוכחית של ההוראה. פורמט ההדפסה דומה לפורמט פורמט Printf בסגנון C, אבל בלי % בהתחלה. פורמט הפרוק עבור ההוראה beq הופכת ל:

    disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"

צריך להוסיף רק שתי הוראות של קפיצה וקישור, jal (דילוג וקישור) jalr (דילוג וקישור עקיף).

ההוראה jal משתמשת בקידוד J-Type:

31 30..21 20 19..12 11..7 6..0
1 10 1 8 5 7
IM IM IM IM שלישי קוד פעולה

בדיוק כמו בהוראות להסתעפות, ההודעה המיידית של 20 סיביות מחולקת מספר שדות, ולכן ייקרא לו jimm20. המקטעים לא חשובים כרגע, אבל נטפל בזה במדריך ליצירת המפענח הבינארי. האופרנד שהמפרט הופך ל-{ : jimm20 : next_pc, rd }. שימו לב שיש שתי גישות אופרנדים של יעד, ערך ה-pc הבא ורשמת הקישור שצוין הוראות להתאמה אישית.

בדומה להוראות ההסתעפות שלמעלה, פורמט הפירוק הופך ל:

    disasm: "jal", "%rd, %(@+jimm20:08x)"

הדילוג והקישור העקיף משתמש בפורמט I-Type עם הפורמט המיידי של 12 ביט. הוא מוסיף את הערך המיידי המורחב עם מספר המספרים השלמים שצוין באמצעות rs1 כדי ליצור את כתובת היעד של ההוראה. רישום הקישור הוא אוספי מספרים שלמים שצוינו ב-rd.

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 שלישי קוד פעולה

אם ראיתם את הדפוס, עכשיו אפשר להסיק שמפרט האופרנד לערך jalr צריך להיות { : rs1, imm12 : next_pc, rd }, ולפירוק מפרט:

    disasm: "jalr", "%rd, %rs1, %imm12"

ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.isa וגם rv32i_instructions.h


הוספת הוראות לחנות

ההוראות לחנות פשוטות מאוד. בכולם נעשה שימוש בפורמט S-Type:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
IM rs2 rs1 func3 IM קוד פעולה

כמו שאפשר לראות, זה עוד מקרה נוסף של קטע מיידי מקוטע ב-12 ביט, קוראים לה simm12. כל ההוראות לאחסון מכילות את הערך של המספר השלם רשמת ב-rs2 לכתובת האפקטיבית בזיכרון שמתקבלת באמצעות הוספת הערך של מרשם המספרים השלמים שצוין על ידי rs1 לערך המורחב של ה-12 ביט המיידי. פורמט האופרנד צריך להיות { : rs1, simm12, rs2 } בשביל את כל ההוראות לחנות.

ההוראות לגבי החנות שצריך להטמיע הן:

  • sb – בייט של אחסון.
  • sh – אחסון חצי מילה.
  • sw - מילת החנות.

מפרט הפרוק של sb הוא כפי שציפיתם:

    disasm: "sb", "%rs2, %simm12(%rs1)"

מפרטי הפונקציות הסמנטיות הן גם מה שציפיתם: "&RV32ISb", וכו'

ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.isa וגם rv32i_instructions.h


הוספת הוראות טעינה

המודלים של הוראות טעינה שונים מעט מהוראות אחרות את הסימולטור. כדי שאפשר יהיה ליצור מודל למקרים שבהם זמן האחזור של הטעינה הוא לא ודאי, הוראות הטעינה מחולקות לשתי פעולות נפרדות: 1) חישוב כתובות וגישה לזיכרון, וגם 2) כתיבה חוזרת של תוצאות. ב אפשר לעשות זאת על ידי פיצול הפעולה הסמנטית של העומס לשניים הוראות נפרדות, ההוראות העיקריות והוראה של ילד. יתרה על כך, כשאנחנו מציינים אופרנדים, צריך לציין אותם גם בשדה הראשי וגם ילד/ה. כדי לעשות את זה, אנחנו מתייחסים למפרט האופרנד כאל רשימת שלשות. התחביר הוא:

{(predicate : sources : destinations), (predicate : sources : destinations), ... }

הוראות הטעינה משתמשות בפורמט I-Type כמו הוראות:

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 שלישי קוד פעולה

מפרט האופרנד מפצל את האופרנדים הדרושים לחישוב הכתובת ליזום את הגישה לזיכרון מיעד הרישום עבור נתוני הטעינה: {( : rs1, imm12 : ), ( : : rd) }

מכיוון שהפעולה הסמנטית מחולקת לשתי הוראות, הפונקציות הסמנטיות צריכים לציין באופן דומה שתי קריאה. עבור lw (טעינת מילה), הערך יהיה כתוב:

    semfunc: "&RV32ILw", "&RV32ILwChild"

מפרט הפרוק קונבנציונלי יותר. אין אזכור של להוראה לילדים. עבור lw, צריך להיות:

    disasm: "lw", "%rd, %imm12(%rs1)"

הוראות הטעינה שצריך להטמיע הן:

  • lb – טעינת בייטים.
  • lbu – טעינת בייטים לא חתומים.
  • lh – טעינת חצי מילה.
  • lhu – טעינה של חצי מילה ללא חתימה.
  • lw – טעינת המילה.

ערכו את השינויים ולאחר מכן תוכלו להמשיך לפתח. בודקים את הפלט שנוצר. יישור כמו קודם, אפשר לבדוק מול riscv32i.isa וגם rv32i_instructions.h

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