מדריך בנושא פונקציות סמנטיות

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

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

סקירה כללית של פונקציות סמנטיות

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

לפונקציה סמנטית יש את החתימה void(Instruction *), כלומר שמעבירה את הסמן למופע של המחלקה Instruction הפונקציה מחזירה את הערך void.

המחלקה Instruction מוגדרת במקומות instruction.h

לצורך כתיבת פונקציות סמנטיות, אנחנו מעוניינים במיוחד הווקטורים של ממשק האופרנד המקור והיעד שאליהם ניגשים קריאות ל-method Source(int i) ו-Destination(int i).

ממשקי האופרנד של המקור והיעד מוצגים למטה:

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

הדרך הבסיסית לכתוב פונקציה סמנטית לאופרנד רגיל 3 כמו הוראת add בגרסת 32-ביט, היא:

void MyAddFunction(Instruction *inst) {
  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);
  uint32_t c = a + b;
  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, c);
  db->Submit();
}

נפרק את החלקים של הפונקציה הזו. שתי השורות הראשונות של גוף הפונקציה קורא מאופרנדים 0 ו-1. הקריאה של AsUint32(0) מפרש את נתוני הבסיס כמערך uint32_t ומאחזר את הערך ה-0 לרכיב מסוים. הדבר נכון גם אם הרישום או הערך הבסיסיים או שהערך של מערך השיעור הוא לא. הגודל (באלמנטים) של אופרנד המקור יכול להיות שמתקבל משיטת אופרנד המקור shape(), שמחזירה וקטור שמכיל את מספר הרכיבים בכל מאפיין. השיטה הזו מחזירה את הערך {1} למערך סקלר, {16} הוא לווקטור רכיב של 16, ו-{4, 4} למערך 4x4.

  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);

אחר כך, למוצר זמני uint32_t בשם c מוקצה הערך a + b.

יכול להיות שבשורה הבאה יהיה צורך בהסבר נוסף:

  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();

DataBuffer הוא אובייקט שנספרת כקובצי עזר ומשמש לאחסון ערכים מצב של סימולציה, כמו רישומים. הוא די לא מקלידים, למרות שיש לו גודל בהתאם לאובייקט שממנו הוא מוקצה. במקרה הזה, הגודל sizeof(uint32_t) ההצהרה הזו מקצה מאגר נתונים חדש בגודל של מאגר נתונים זמני היעד של אופרנד היעד הזה – במקרה הזה רישום מספרים שלמים בגרסת 32 ביט. ה-DataBuffer מאותחל גם עם זמן אחזור ארכיטקטוני של ההוראה. הנתון הזה מצוין במהלך ההוראה לפענח אותו.

השורה הבאה מתייחסת למופע של מאגר הנתונים הזמני בתור מערך של uint32_t ו כותבת את הערך שמאוחסן ב-c לרכיב ה-0.

  db->Set<uint32_t>(0, c);

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

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

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument1, Argument2)> operation) {
  Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
  Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument, Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Result, Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

תוכלו לראות שבמקום להשתמש בהצהרה כמו:

  uint32_t a = inst->Source(0)->AsUint32(0);

פונקציית העזרה משתמשת במונחים הבאים:

generic::GetInstructionSource<Argument>(instruction, 0);

GetInstructionSource היא קבוצה של פונקציות עזר שמופיעות בתבנית משמשים כדי לספק שיטות גישה בתבנית למקור ההוראה אופרנדים. בלעדיהם, כל אחת מפונקציות העזרה הייתה ספציפית לכל סוג כדי לגשת לאופרנד המקור עם As<int type>(). אפשר לראות את ההגדרות של התבניות האלה פועל ב: instruction.h

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

void MyAddFunction(Instruction *inst) {
  generic::BinaryOp<uint32_t>(inst,
                              [](uint32_t a, uint32_t b) { return a + b; });
}

אחרי הידור באמצעות bazel build -c opt ו-copts = ["-O3"] ב-build הוא אמור להיות מוטבע לגמרי ללא תקורה, וכך אנחנו מקבלים תמציתיות ללא פגיעה בביצועים.

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


פיתוח ראשוני

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

$  bazel build :riscv32i
...<snip>...

לא נוצרים קבצים, אז זו רק הרצה מוודאים שהכול בסדר.


הוספת שלוש הוראות ל-ALU באופרנד

עכשיו נוסיף את הפונקציות הסמנטיות ל-ALU גנרי בן 3 אופרנדים הוראות להתאמה אישית. פותחים את הקובץ rv32i_instructions.cc ומוודאים שהכול תקין הגדרות חסרות מתווספות לקובץ rv32i_instructions.h עם ההתקדמות.

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

  • add – הוספת מספר שלם בגרסת 32 ביט.
  • and – ברמת 32 ביט וגם.
  • or – ברמת 32 ביט או.
  • sll – שינוי לוגי של 32 ביט שמאלה.
  • sltu – ערך לא חתום של 32 ביט מוגדר 'פחות מ-'.
  • sra – הזזת חשבון ימינה של 32 ביט.
  • srl – מעבר ימינה לוגי של 32 ביט.
  • sub – חיסור ממספר שלם של 32 ביט.
  • xor – xor של 32 ביט.

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

למעט sra, ניתן להתייחס לכל ההוראות שלמעלה כפעולה ערכים לא חתומים של 32 ביט, כך שניתן להשתמש בהם בפונקציית התבנית BinaryOp קודם לכן בדקנו עם הארגומנט של סוג התבנית היחיד. ממלאים את עצמו ב-rv32i_instructions.cc בהתאם. שימו לב שרק 5 הדרגות הנמוכות הביטים של האופרנד השני בהוראות ה-Shift משמשים להחלפה לסכום. אחרת, כל הפעולות יהיו בפורמט src0 op src1:

  • add: a + b
  • and: a & b
  • or : a | b
  • sll : a << (b & 0x1f)
  • sltu : (a < b) ? 1 : 0
  • srl : a >> (b & 0x1f)
  • sub : a - b
  • xor : a ^ b

בשביל sra נשתמש בתבנית BinaryOp של שלושת הארגומנטים. מסתכלים על תבנית, הארגומנט הראשון הוא סוג התוצאה uint32_t. השנייה היא אופרנד מקור 0, במקרה הזה int32_t, והאחרון הוא הסוג. של אופרנד מקור 1, במקרה הזה uint32_t. זה הופך את הגוף של sra פונקציה סמנטית:

  generic::BinaryOp<uint32_t, int32_t, uint32_t>(
      instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });

בצעו את השינויים כדי לשפר את התוצאות. אפשר לבדוק את העבודה מול rv32i_instructions.cc


הוספת שתי הוראות ALU לאופרנד

יש רק שתי הוראות ל-ALU בשני אופרנדים: lui ו-auipc. הקודם מעתיק את אופרנד המקור שהועבר מראש ישירות ליעד. האחרון מוסיף את כתובת ההוראה באופן מיידי לפני כתיבתה היעד. אפשר לגשת לכתובת ההוראה מה-method address() של האובייקט Instruction.

מכיוון שיש רק אופרנד מקור אחד, אנחנו לא יכולים להשתמש ב-BinaryOp. אנחנו צריכים להשתמש ב-UnaryOp. מכיוון שאנחנו יכולים להתייחס גם למקור אופרטורים של יעד בתור uint32_t יכולים להשתמש בתבנית של ארגומנט יחיד .

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

גוף הפונקציה הסמנטית של lui פחות טריוויאלי, פשוט להחזיר את המקור. הפונקציה הסמנטית של auipc מציגה קטין כי עליך לגשת ל-method address() ב-Instruction מכונה. התשובה היא להוסיף את instruction לצילומי ה-lambda שזמינות לשימוש בגוף הפונקציה lambda. במקום [](uint32_t a) { ... } כמו קודם, צריך לכתוב את ה-lambda ב-[instruction](uint32_t a) { ... }. עכשיו ניתן להשתמש בפונקציה instruction בגוף lambda.

בצעו את השינויים כדי לשפר את התוצאות. אפשר לבדוק את העבודה מול rv32i_instructions.cc


הוספת הוראות לשינוי תהליך הבקרה

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

הוספת הוראות להסתעפות מותנית

אין פונקציית עזר להוראה הסתעפות, לכן יש שתי אפשרויות. כותבים את הפונקציות הסמנטיות מאפס או כותבים פונקציה מסייעת מקומית. מכיוון שאנחנו צריכים ליישם 6 הוראות הסתעפות, נראה שהאחרונה שווה במאמץ מסוים. לפני שנעשה את זה, נבחן את ההטמעה של הסתעפות של פונקציה סמנטית של הוראה מאפס.

void MyConditionalBranchGreaterEqual(Instruction *instruction) {
  int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
  int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
  if (a >= b) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0,m target);
    db->Submit();
  }
}

הדבר היחיד שמשתנה בהתאם להוראות להסתעפות הוא ההסתעפות ואת סוגי הנתונים, חתומים לעומת לא חתומים של 32 ביט, מבין השניים אופרנדים במקור. זה אומר שאנחנו צריכים פרמטר תבנית עבור המודל אופרנדים במקור. פונקציית העזרה עצמה צריכה לבצע את הפקודה Instruction ואובייקט שאפשר לקרוא אליו, כמו std::function שמחזיר bool בתור פרמטרים. פונקציית העזרה תיראה כך:

template <typename OperandType>
static inline void BranchConditional(
    Instruction *instruction,
    std::function<bool(OperandType, OperandType)> cond) {
  OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
  OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
  if (cond(a, b)) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0, target);
    db->Submit();
  }
}

עכשיו אפשר לכתוב את הפונקציה הסמנטית bge (הסתעפות חתומה גדולה או שווה) כ:

void RV32IBge(Instruction *instruction) {
  BranchConditional<int32_t>(instruction,
                             [](int32_t a, int32_t b) { return a >= b; });
}

בהמשך מפורטות ההוראות להסתעפות:

  • Beq – הסתעפות שווה.
  • Bgeu – הסתעפות גדולה או שווה (לא חתומה).
  • Blt - הסתעפות קטן מ (חתום).
  • Bltu - הסתעפות פחות מ (לא חתום).
  • Bne – הסתעפות לא שווה.

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

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

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

ההוראה jalr מתבססת על רשם בסיס כאופרנד מקור 0 וקיזוז כאופרנד מקור 1, ומחבר אותם יחד כדי לחשב את יעד הקפיצה. אחרת, היא זהה להוראה jal.

בהתבסס על התיאורים האלה של הסמנטיקה של ההוראות, כותבים את שני הערכים ו-build. אפשר לבדוק את העבודה מול rv32i_instructions.cc


הוספת הוראות לאחסון הזיכרון

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

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

נבחן את הפונקציה הסמנטית של ההוראה fence כבר הטמענו ב-rv32i_instructions.cc כדוגמה. fence מחזיקה בעיית הוראה עד שפעולות זיכרון מסוימות הושלמו. הוא משמש כדי להבטיח את סדר הזיכרון בין ההוראות שמבצעים לפני ההוראה וגם את אלה שמבצעים אחריה.

// Fence.
void RV32IFence(Instruction *instruction) {
  uint32_t bits = instruction->Source(0)->AsUint32(0);
  int fm = (bits >> 8) & 0xf;
  int predecessor = (bits >> 4) & 0xf;
  int successor = bits & 0xf;
  auto *state = static_cast<RiscVState *>(instruction->state());
  state->Fence(instruction, fm, predecessor, successor);
}

החלק המרכזי בפונקציה הסמנטית של ההוראה fence הוא קודם כל אובייקט המצב מאוחזר באמצעות method ב-Instruction ו-downcast<> לסיווג נגזר ספציפי ל-RiscV. ואז Fence ה-method RiscVState נקראת 'ביצוע פעולת הגדרות'.

ההוראות לחנות יפעלו באופן דומה. הראשונה היא הכתובת האפקטיבית של גישת הזיכרון מחושבת מהאופרנדים של מקור ההוראה והקיזוז מהבסיס, הערך שיישמר יאוחזר מהאופרנד הבא של המקור. בשלב הבא, אובייקט מצב RiscV מתקבל באמצעות הפעלת השיטה state() ו static_cast<>, והשיטה המתאימה נקראת.

ה-method StoreMemory של האובייקט RiscVState היא פשוטה יחסית, אבל יש לה יש השלכות מסוימות שעלינו להיות מודעים להן:

  void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);

כמו שאפשר לראות, השיטה לוקחת שלושה פרמטרים, הסמן אל החנות ההוראה עצמה, כתובת החנות ומצביע אל DataBuffer שמכיל את נתוני החנות. שימו לב, אין צורך לציין גודל, המכונה DataBuffer עצמה מכילה method size(). עם זאת, אין אופרנד יעד הנגיש להוראה שניתן להשתמש בה להקצות מופע DataBuffer בגודל המתאים. במקום זאת, אנחנו צריכים משתמשים במפעל DataBuffer שמתקבל מהשיטה db_factory() במופע Instruction. במפעל יש שיטה Allocate(int size) מחזירה מופע DataBuffer בגודל הנדרש. כאן מוצגת דוגמה איך להשתמש בה כדי להקצות מכונת DataBuffer לחנות חצי מילים (שימו לב: auto היא תכונת C++ שמסיקה את הסוג מהיד הימנית צד המטלה):

  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(uint16_t));

אחרי שיש לנו את המכונה DataBuffer אפשר לכתוב לה כרגיל:

  db->Set<uint16_t>(0, value);

לאחר מכן מעבירים אותו לממשק של מאגר הזיכרון:

  state->StoreMemory(instruction, address, db);

עדיין לא סיימנו. המופע של DataBuffer נספר כהפניה. הזה בדרך כלל מובנת ומטופלת באמצעות השיטה Submit, כדי לשמור את התרחיש לדוגמה והתדיר ביותר, ופשוט ככל האפשר. עם זאת, StoreMemory אינו שכתוב בצורה הזו. הפעולה IncRef תכסה את המכונה DataBuffer בזמן שהיא פועלת עליו ואז DecRef בסיום. אבל אם הפונקציה הסמנטית לא DecRef עצמו. לכן, השורה האחרונה להיות:

  db->DecRef();

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

template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->Set<ValueType>(0, value);
  state->StoreMemory(instruction, address, db);
  db->DecRef();
}

מסיימים את הפונקציות הסמנטיות של החנות ו-build. אפשר לבדוק את לפעול כנגד rv32i_instructions.cc


הוספת הוראות לטעינת זיכרון

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

  • lb - טעינת בייט, כניסה למילה.
  • lbu - טעינת בייט ללא חתימה, אפס הרחבה למילה.
  • lh – טעינה חצי מילה, הרחבה למילה.
  • lhu - טעינה של חצי מילה ללא חתימה, והרחבה של אפס למילה.
  • lw – טעינת המילה.

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

נתחיל בהצהרה על השיטה LoadMemory בRiscVState:

  void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
                  Instruction *child_inst, ReferenceCount *context);

בהשוואה לשיטה StoreMemory, ב-LoadMemory נדרשים שני שלבים נוספים פרמטרים: מצביע למופע של Instruction ומצביע אל נספרה הפניה כאובייקט context. הראשונה היא הוראה של ילד מממשת את תהליך הכתיבה החוזרת (call-back) (כפי שמתואר במדריך של מפענח ISA). הוא מתבצעת גישה באמצעות ה-method child() במכונה הנוכחית של Instruction. האפשרות השנייה היא מצביעה למופע של מחלקה שנגזרת ReferenceCount, שבמקרה הזה מאחסן מופע DataBuffer מכילים את הנתונים שנטענו. אובייקט ההקשר זמין דרך השיטה context() באובייקט Instruction (אם כי ברוב ההוראות הערך שמוגדר לפרמטר הזה הוא nullptr).

אובייקט ההקשר לטעינת זיכרון RiscV מוגדר בתור המבנה הבא:

// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
  explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
  ~LoadContext() override {
    if (value_db != nullptr) value_db->DecRef();
  }

  // Override the base class method so that the data buffer can be DecRef'ed
  // when the context object is recycled.
  void OnRefCountIsZero() override {
    if (value_db != nullptr) value_db->DecRef();
    value_db = nullptr;
    // Call the base class method.
    generic::ReferenceCount::OnRefCountIsZero();
  }
  // Data buffers for the value loaded from memory (byte, half, word, etc.).
  DataBuffer *value_db = nullptr;
};

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

template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->set_latency(0);
  auto *context = new riscv::LoadContext(db);
  state->LoadMemory(instruction, address, db, instruction->child(), context);
  context->DecRef();
}

כמו שאפשר לראות, ההבדל העיקרי הוא שהמכונה של DataBuffer שהוקצתה לו. מועבר גם לקריאה LoadMemory כפרמטר, וגם שמור אובייקט LoadContext.

הפונקציות הסמנטיות בהוראה של צאצא דומות מאוד. קודם כל, הפרמטר LoadContext מתקבל באמצעות קריאה ל-method Instruction context(), הועבר באופן סטטי ל-LoadContext *. שנית, הערך (בהתאם לנתונים type) נקרא מהמכונה DataBuffer של נתוני הטעינה. השלישי, מכונה אחת (DataBuffer) מוקצית מאופרנד היעד. לבסוף, הערך שנטען נכתב למכונה החדשה של DataBuffer, ומופיע ב-Submit. שוב, כדאי להשתמש בתבנית מסייעת באמצעות תבנית:

template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
  auto *context = down_cast<riscv::LoadContext *>(instruction->context());
  uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, value);
  db->Submit();
}

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

אפשר לבדוק את העבודה מול rv32i_instructions.cc


בנייה והפעלה של הסימולטור הסופי

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

צריך לשנות את ספריית העבודה ל-other/ וליצור. צריך לבנות בלי שגיאות.

$ cd ../other
$ bazel build :rv32i_sim

בספרייה הזו יש 'שלום עולם' פשוט תוכנה בקובץ hello_rv32i.elf כדי להריץ את הסימולטור על הקובץ הזה ולראות את התוצאות:

$ bazel run :rv32i_sim -- other/hello_rv32i.elf

אתם אמורים לראות משהו דומה לדברים הבאים:

INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$

ניתן להריץ את הסימולטור גם במצב אינטראקטיבי באמצעות הפקודה bazel run :rv32i_sim -- -i other/hello_rv32i.elf. מתקבלת דוגמה מעטפת הפקודה. צריך להקליד help בהודעה כדי לראות את הפקודות הזמינות.

$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000   addi           ra, 0, 0
[0] > help


    quit                           - exit command shell.
    core [N]                       - direct subsequent commands to core N
                                     (default: 0).
    run                            - run program from current pc until a
                                     breakpoint or exit. Wait until halted.
    run free                       - run program in background from current pc
                                     until breakpoint or exit.
    wait                           - wait for any free run to complete.
    step [N]                       - step [N] instructions (default: 1).
    halt                           - halt a running program.
    reg get NAME [FORMAT]          - get the value or register NAME.
    reg NAME [FORMAT]              - get the value of register NAME.
    reg set NAME VALUE             - set register NAME to VALUE.
    reg set NAME SYMBOL            - set register NAME to value of SYMBOL.
    mem get VALUE [FORMAT]         - get memory from location VALUE according to
                                     format. The format is a letter (o, d, u, x,
                                     or X) followed by width (8, 16, 32, 64).
                                     The default format is x32.
    mem get SYMBOL [FORMAT]        - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem SYMBOL [FORMAT]            - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem set VALUE [FORMAT] VALUE   - set memory at location VALUE(1) to VALUE(2)
                                     according to FORMAT. Default format is x32.
    mem set SYMBOL [FORMAT] VALUE  - set memory at location SYMBOL to VALUE
                                     according to FORMAT. Default format is x32.
    break set VALUE                - set breakpoint at address VALUE.
    break set SYMBOL               - set breakpoint at value of SYMBOL.
    break VALUE                    - set breakpoint at address VALUE.
    break SYMBOL                   - set breakpoint at value of SYMBOL.
    break clear VALUE              - clear breakpoint at address VALUE.
    break clear SYMBOL             - clear breakpoint at value of SYMBOL.
    break clear all                - remove all breakpoints.
    help                           - display this message.

_start:
80000000   addi           ra, 0, 0
[0] >

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