מדריך למפענח הוראה בינארי

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

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

סקירה כללית

קידוד של הוראות בינאריות ב-RiscV

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

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

פורמטים ההוראות שבהם נעשה שימוש בהוראות שאנחנו רוצים לפענח בסימולטור RiscV הם:

פורמט מסוג R, המשמש להוראות של רגיסטרים לרגיסטרים:

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

פורמט מסוג I, המשמש להוראות של זיכרון מיידי, להוראות טעינה ולהוראה jalr, 12 ביט מיידיים.

31..20 19..15 14..12 11.7 6.0
12 5 3 5 7
imm12 rs1 func3 rd קוד פעולה

פורמט I-Type מיוחד, המשמש למשמרת עם הוראות מיידיות, 5 ביט מיידי:

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

פורמט מסוג U, המשמש להוראות מיידיות ארוכות (lui, auipc), מיידיות של 20 ביט:

31..12 11.7 6.0
20 5 7
uimm20 rd קוד פעולה

פורמט מסוג B, המשמש להסתעפויות מותנות, מיידיות של 12 ביט.

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

פורמט J-Type, משמש להוראה בנושא jal, 20 ביט מיידי.

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

פורמט מסוג S, המשמש להוראות אחסון, מיידיות באורך 12 ביט.

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

כפי שאפשר לראות מהפורמטים האלה, כל ההוראות האלה באורך 32 ביט, ו-7 הביטים הנמוכים בכל פורמט הם שדה קוד הפקודה. שימו לב גם שלמרות שלכמה פורמטים יש ערכים מיידיים באותו גודל, הביטים שלהם נלקחים מחלקים שונים של ההוראה. כפי שנראה בהמשך, פורמט המפרט של המפענח הבינארי יכול לבטא זאת.

תיאור הקידוד הבינארי

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

במדריך הזה נכתוב קובץ תיאור של קידוד בינארי לקבוצת משנה של ההוראות של RiscV32I שנדרשות כדי לדמות את ההוראות שבתוכנית 'Hello World' הקטנה. פרטים נוספים על ISA של RiscV זמינים במפרטי Risc-V{.external}.

קודם פותחים את הקובץ: riscv_bin_decoder/riscv32i.bin_fmt.

תוכן הקובץ מחולק לכמה חלקים.

הראשונה היא ההגדרה של decoder.

decoder RiscV32I {
  // The namespace in which code will be generated.
  namespace mpact::sim::codelab;
  // The name (including any namespace qualifiers) of the opcode enum type.
  opcode_enum = "OpcodeEnum";
  // Include files specific to this decoder.
  includes {
    #include "riscv_isa_decoder/solution/riscv32i_decoder.h"
  }
  // Instruction groups for which to generate decode functions.
  RiscVInst32;
};

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

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

// The generic RiscV 32 bit instruction format.
format Inst32Format[32] {
  fields:
    unsigned bits[25];
    unsigned opcode[7];
};

// RiscV 32 bit instruction format used by a number of instructions
// needing a 12 bit immediate, including CSR instructions.
format IType[32] : Inst32Format {
  fields:
    signed imm12[12];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

// RiscV instruction format used by fence instructions.
format Fence[32] : Inst32Format {
  fields:
    unsigned fm[4];
    unsigned pred[4];
    unsigned succ[4];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

הראשון מגדיר פורמט של הוראות ברוחב 32 ביט בשם Inst32Format, שיש לו שני שדות: bits (רוחב 25 ביט) ו-opcode (רוחב 7 ביט). כל שדה הוא unsigned, כלומר הערך יורחב לאפס אחרי חילוץ הנתונים וימוקם בסוג מספר שלם C++. סכום מידות הרוחב של שדות הביטים חייב להיות שווה לרוחב הפורמט. הכלי יפיק שגיאה אם יש מחלוקת. הפורמט הזה לא נגזר מפורמט אחר, ולכן הוא נחשב לפורמט ברמה העליונה.

השיטה השנייה מגדירה פורמט הוראה רחב ב-32 ביט בשם IType שנגזר מ-Inst32Format, ושני הפורמטים האלה קשורים. הפורמט מכיל 5 שדות: imm12,‏ rs1,‏ func3,‏ rd ו-opcode. השדה imm12 הוא signed, כלומר הערך יורחב לאחר חילוץ הערך וימוקם מסוג מספר שלם C++. שימו לב של-IType.opcode יש את אותו מאפיין signed/unsigned, והוא מתייחס לאותם ביטים של מילה של הוראה כמו Inst32Format.opcode.

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

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

אחרי הגדרות הפורמט ב-riscv32i.bin_fmt מופיעה הגדרה של קבוצת הוראות. לכל ההוראות בקבוצת הוראות צריך להיות אותו אורך בייט, והן צריכות להשתמש בפורמט שמבוסס (אולי באופן עקיף) על אותו פורמט של הוראות ברמה העליונה. כש-ISA יכול לכלול הוראות באורך שונה, נעשה שימוש בקבוצת הוראות שונה לכל אורך. בנוסף, אם פענוח ה-ISA של היעד תלוי במצב ביצוע, כמו הוראות Arm לעומת הוראות Thumb, נדרשת קבוצת הוראות נפרדת לכל מצב. מנתח bin_fmt יוצר מפענח בינארי לכל קבוצת הוראות.

instruction group RiscV32I[32] "OpcodeEnum" : Inst32Format {
  fence   : Fence  : func3 == 0b000, opcode == 0b000'1111;
  csrs    : IType  : func3 == 0b010, rs1 != 0, opcode == 0b111'0011;
  csrw_nr : IType  : func3 == 0b001, rd == 0,  opcode == 0b111'0011;
  csrs_nw : IType  : func3 == 0b010, rs1 == 0, opcode == 0b111'0011;
};

קבוצת ההוראות מגדירה שם RiscV32I, רוחב [32], את השם של סוג המניין של opcode שבו רוצים להשתמש "OpcodeEnum" ותבנית של הוראה בסיסית. סוג ספירת ה-opcode צריך להיות זהה לזה שנוצר על ידי מפענח ההוראה העצמאית של הפורמט, שמופיע במדריך במפענח ISA.

כל תיאור של קידוד הוראות מורכב מ-3 חלקים:

  • שם קוד הפקודה, שצריך להיות זהה לשם שצוין בתיאור של מפענח ההוראות כדי שהשניים יפעלו יחד.
  • פורמט ההוראה שבו יש להשתמש עבור קוד הפקודה. זהו הפורמט שמשמש כדי לספק הפניות לשדות ביטים בחלק האחרון.
  • רשימה מופרדת בפסיקים של אילוצים של שדות ביט, ==,‏ !=,‏ <,‏ <=,‏ > ו->=, שכל אחד מהם חייב להיות נכון כדי שקוד הפעולה יתאים למילה המבצעת.

מנתח ה-.bin_fmt משתמש בכל המידע הזה כדי ליצור מפענח שמבצע את הפעולות הבאות:

  • הפונקציה מספקת פונקציות חילוץ (signed/unsigned) בהתאם לכל שדה ביט בכל פורמט. פונקציות החילוץ ממוקמות במרחבי שמות בעלי שם בגרסת אות הנחש של שם הפורמט. לדוגמה, פונקציות החילוץ של הפורמט IType ממוקמות במרחב השמות i_type. כל פונקציית חילוץ מוצהרת כ-inline, מקבלת את סוג uint_t הצר ביותר שמכיל את רוחב הפורמט ומחזירה את סוג int_t (לסימון) או uint_t (ללא סימון) הצר ביותר שמכיל את רוחב השדה שחולץ. E.g.:
inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}
  • פונקציית פענוח לכל קבוצת הוראות. הפונקציה מחזירה ערך מסוג OpcodeEnum, ומשתמשת בסוג uint_t הצר ביותר שמכיל את הרוחב של פורמט קבוצת ההוראות.

ביצוע build ראשוני

עוברים לספרייה riscv_bin_decoder ובונים את הפרויקט באמצעות הפקודה הבאה:

$ cd riscv_bin_decoder
$ bazel build :all

עכשיו עוברים בספרייה חזרה לשורש המאגר, בודקים את המקורות שנוצרו. לשם כך, צריך לשנות את הספרייה ל-bazel-out/k8-fastbuild/bin/riscv_bin_decoder (בהנחה שאתם נמצאים במארח x86 – למארחים אחרים, k8-fastbuild יהיה מחרוזת נוספת).

$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_bin_decoder
  • riscv32i_bin_decoder.h
  • riscv32i_bin_decoder.cc

קובץ הכותרת שנוצר (‎.h)

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

#ifndef RISCV32I_BIN_DECODER_H
#define RISCV32I_BIN_DECODER_H

#include <iostream>
#include <cstdint>

#include "third_party/absl/functional/any_invocable.h"


#include "learning/brain/research/mpact/sim/codelab/riscv_isa_decoder/solution/riscv32i_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {


namespace internal {

template <typename T>
static inline T ExtractBits(const uint8_t *data, int data_size,
                            int bit_index, int width) {
  if (width == 0) return 0;

  int byte_pos = bit_index >> 3;
  int end_byte = (bit_index + width - 1) >> 3;
  int start_bit = bit_index & 0x7;

  // If it is only from one byte, extract and return.
  if (byte_pos == end_byte) {
    uint8_t mask = 0xff >> start_bit;
    return (mask & data[byte_pos]) >> (8 - start_bit - width);
  }

  // Extract from the first byte.
  T val = 0;
  val = data[byte_pos++] & 0xff >> start_bit;
  int remainder = width - (8 - start_bit);
  while (remainder >= 8) {
    val = (val << 8) | data[byte_pos++];
    remainder -= 8;
  }

  // Extract any remaining bits.
  if (remainder > 0) {
    val <<= remainder;
    int shift = 8 - remainder;
    uint8_t mask = 0b1111'1111 << shift;
    val |= (data[byte_pos] & mask) >> shift;
  }
  return val;
}

}  // namespace internal

אחרי הקטע הראשוני מופיעה קבוצה של שלושה מרחבי שמות, אחד לכל אחת מההצהרות format בקובץ riscv32i.bin_fmt:


namespace fence {

...

}  // namespace fence

namespace i_type {

...

}  // namespace i_type

namespace inst32_format {

...

}  // namespace inst32_format

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

הפונקציות במרחב השמות i_type מוצגות למטה:

namespace i_type {

inline uint8_t ExtractFunc3(uint32_t value) {
  return  (value >> 12) & 0x7;
}

inline int16_t ExtractImm12(uint32_t value) {
  int16_t result = ( (value >> 20) & 0xfff) << 4;
  result = result >> 4;
  return result;
}

inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}

inline uint8_t ExtractRd(uint32_t value) {
  return  (value >> 7) & 0x1f;
}

inline uint8_t ExtractRs1(uint32_t value) {
  return  (value >> 15) & 0x1f;
}

}  // namespace i_type

לבסוף, מופיעה ההצהרה על הפונקציה של פונקציית הפענוח של קבוצת ההוראות RiscVInst32. הפונקציה מקבלת ערך של 32 ביט ללא סימן בתור הערך של מילת ההוראה ומחזירה את חבר סיווג המניין OpcodeEnum שתואם, או את הערך OpcodeEnum::kNone אם אין התאמה.

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

קובץ המקור שנוצר (‎.cc)

עכשיו פותחים את riscv32i_bin_decoder.cc. החלק הראשון של הקובץ מכיל את ההצהרות על #include ועל מרחב השמות, ואחריו ההצהרות על פונקציית הפענוח:

#include "riscv32i_bin_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {

OpcodeEnum DecodeRiscVInst32None(uint32_t);
OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word);

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

absl::AnyInvocable<OpcodeEnum(uint32_t)> parse_group_RiscVInst32_0[kParseGroupRiscVInst32_0_Size] = {
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32_0_3,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,

    ...

    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32_0_3c, &DecodeRiscVInst32None,

    ...
};

לבסוף, מגדירים את פונקציות המפענח:

OpcodeEnum DecodeRiscVInst32None(uint32_t) {
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word) {
  if ((inst_word & 0x4003) != 0x3) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 2) & 0x1f;
  index |= (inst_word >> 7) & 0x60;
  return parse_group_RiscVInst32_0[index](inst_word);
}

OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word) {
  return OpcodeEnum::kFence;
}

OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word) {
  if ((inst_word & 0xf80) != 0x0) return OpcodeEnum::kNone;
  return OpcodeEnum::kCsrwNr;
}

OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word) {
  uint32_t rs1_value = (inst_word >> 15) & 0x1f;
  if (rs1_value != 0x0)
    return OpcodeEnum::kCsrs;
  if (rs1_value == 0x0)
    return OpcodeEnum::kCsrsNw;
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word) {
  OpcodeEnum opcode;
  opcode = DecodeRiscVInst32_0(inst_word);
  return opcode;
}

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


הוספת הוראות ALU מסוג register-register

עכשיו הגיע הזמן להוסיף כמה הוראות חדשות לקובץ riscv32i.bin_fmt. הקבוצה הראשונה של ההוראות היא הוראות 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 rd קוד פעולה

השלב הראשון הוא להוסיף את הפורמט. פותחים את הקובץ riscv32i.bin_fmt בכלי העריכה המועדף עליכם. אחרי ה-Inst32Format אפשר להוסיף פורמט שנקרא RType שמבוסס על Inst32Format. כל שדות הבייטים ב-RType הם unsigned. כדי להגדיר את הפורמט, משתמשים בשמות, ברוחב הביטים ובסדר (משמאל לימין) שמופיעים בטבלה שלמעלה. לקבלת רמז או כדי לראות את הפתרון המלא, לחצו כאן.

בשלב הבא צריך להוסיף את ההוראות. ההוראות הן:

  • add – הוספת מספר שלם.
  • and – AND ברמת הביטים.
  • or – או ברמת הביטים.
  • sll – shift שמאלי לוגי.
  • sltu – מוגדר פחות מ-, ללא סימן.
  • sub – הפחתה של מספר שלם.
  • xor – XOR ברמת הביטים.

הקידודים שלהם:

31..25 24..20 19..15 14..12 11.7 6..0 שם קוד הפעולה
000 0000 rs2 rs1 000 rd 011 0011 add
000 0000 rs2 rs1 111 rd 011 0011 וגם
000 0000 rs2 rs1 110 rd 011 0011 או
000 0000 rs2 rs1 001 rd 011 0011 sll
000 0000 rs2 rs1 011 rd 011 0011 sltu
010 0000 rs2 rs1 000 rd 011 0011 sub
000 0000 rs2 rs1 100 שלישי 011 0011 xor
func7 func3 קוד פעולה

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

בכל אחת מהגדרות ההוראות האלה יהיו שלושה אילוצים, כלומר func7, func3 ו-opcode. לכל הערכים מלבד sub, האילוץ func7 יהיה:

func7 == 0b000'0000

האילוץ func3 משתנה ברוב ההוראות. עבור add ו-sub, הערך הוא:

func3 == 0b000

האילוץ opcode זהה בכל אחת מההוראות האלה:

opcode == 0b011'0011

חשוב לסיים כל שורה בנקודה-פסיק ;.

הפתרון המוגמר נמצא כאן.

עכשיו אפשר לבנות את הפרויקט כמו קודם ולפתוח את הקובץ riscv32i_bin_decoder.cc שנוצר. תוכלו לראות שנוצרו פונקציות נוספות של מקודדים כדי לטפל בהוראות החדשות. ברוב המקרים הם דומים לאלה שנוצרו קודם, אבל כדאי להסתכל על DecodeRiscVInst32_0_c שמשמש לפענוח של add/sub:

OpcodeEnum DecodeRiscVInst32_0_c(uint32_t inst_word) {
  static constexpr OpcodeEnum opcodes[2] = {
    OpcodeEnum::kAdd,
    OpcodeEnum::kSub,
  };
  if ((inst_word & 0xbe000000) != 0x0) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 30) & 0x1;
  return opcodes[index];
}

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


הוספת הוראות 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 rd קוד פעולה

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

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

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

31..12 11.7 6.0
20 5 7
uimm20 rd קוד פעולה

הפורמט I-Type כבר קיים ב-riscv32i.bin_fmt, כך שאין צורך להוסיף את הפורמט הזה.

אם משווים בין הפורמט המיוחד של סוג I לבין הפורמט של סוג R שהגדרנו בתרגיל הקודם, רואים שההבדל היחיד הוא ששדה rs2 עבר לשם uimm5. במקום להוסיף פורמט חדש לגמרי, אפשר להרחיב את הפורמט R-Type. אי אפשר להוסיף עוד שדה, כי זה יגדיל את רוחב הפורמט, אבל אפשר להוסיף שכבת-על. שכבת-על היא כינוי לקבוצת ביטים בפורמט, וניתן להשתמש בה כדי לשלב כמה רצפי משנה של הפורמט לישות נפרדת בעלת שם. תופעת הלוואי היא שהקוד שנוצר יכלול עכשיו גם פונקציית חילוץ לשכבת-העל, בנוסף לפונקציות החילוץ של השדות. במקרה הזה, אם גם rs2 וגם uimm5 לא חתומים אין הבדל משמעותי, חוץ מאשר לציין במפורש שהשדה משמש כפעולה מיידית. כדי להוסיף שכבת-על בשם uimm5 לפורמט מסוג R, מוסיפים את הטקסט הבא אחרי השדה האחרון:

  overlays:
    unsigned uimm5[5] = rs2;

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

  overlays:
    unsigned uimm32[32] = uimm20, 0b0000'0000'0000;

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

אלה ההוראות מסוג I שצריך להוסיף:

  • addi – הוספה מיידית.
  • andi – סיביות ובאופן מיידי.
  • ori – ברמת הביטים או עם מיידיות.
  • xori – XOR בינארי עם ערך מיידי.

הקידודים שלהם:

31..20 19..15 14..12 11.7 6.0 opcode_name
imm12 rs1 000 rd 001 0011 addi
imm12 rs1 111 rd 001 0011 andi
imm12 rs1 110 rd 001 0011 אורי
imm12 rs1 100 שלישי 001 0011 Xori
func3 קוד פעולה

הוראות ה-R-Type (הוראות I-Type מיוחדות) שצריך להוסיף הן:

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

הקידודים שלהם הם:

31..25 24..20 19..15 14..12 11.7 6..0 שם קוד הפעולה
000 0000 uimm5 rs1 001 rd 001 0011 slli
010 0000 uimm5 rs1 101 rd 001 0011 srai
000 0000 uimm5 rs1 101 rd 001 0011 srli
func7 func3 קוד פעולה

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

  • auipc – הוספת 'מיידי עליון' למחשב.
  • lui – טעינה מיידית של נתונים מהזיכרון העליון.

הקידודים שלהם הם:

31..12 11.7 6..0 שם opcode
uimm20 rd 001 0111 auipc
uimm20 שלישי 011 0111 lui
קוד פעולה

מבצעים את השינויים הרצויים ומבצעים build. בודקים את הפלט שנוצר. בדיוק כמו קודם, אתם יכולים לבדוק את העבודה שלכם מול riscv32i.bin_fmt.


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

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

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

אמנם הקידוד B-Type זהה בפריסה לקידוד R-Type, אבל אנחנו בוחרים להשתמש בסוג פורמט חדש כדי שיתאים למסמכי התיעוד של RiscV. אבל אפשר גם להוסיף שכבת-על כדי לקבל את ההזזה המתאימה של ההסתעפות באופן מיידי, באמצעות השדות func7 ו-rd של קידוד R-Type.

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

רצף הביטים במילה של ההוראה שמשמשת ליצירת הערך המיידי הוא: 31,‏ 7,‏ 30..25,‏ 11..8. זה תואם להפניות הבאות לשדות משנה, שבהן האינדקס או הטווח מציינים את הביטים בשדה, שממוספרים מימין לשמאל, כלומר: imm7[6] מתייחס ל-msb של imm7, ו-imm5[0] מתייחס ל-lsb של imm5.

imm7[6], imm5[0], imm7[5..0], imm5[4..1]

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

  overlays:
    signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;

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

ההוראה 'קפיצה וקישור' (מיידית) משתמשת בקידוד מסוג J:

31..12 11.7 6.0
20 5 7
imm20 rd קוד פעולה

גם הפורמט הזה קל להוספה, אבל שוב, המאפיין המיידי שבו משתמשת ההוראה לא פשוט כמו שהוא נראה. רצפי הביטים שמשמשים ליצירת המאפיין המיידי המלא הם: 31,‏ 19..12,‏ 20,‏ 30..21, והמאפיין המיידי הסופי מוסט שמאלה באחד לצורך התאמה למחצית מילה. הפתרון הוא להוסיף שכבת-על נוספת (21 ביט כדי להביא בחשבון את ההזזה ימינה) לפורמט:

  overlays:
    signed j_imm[21] = imm20[19, 7..0, 8, 18..9], 0b0;

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

    signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;

לבסוף, הדילוג והקישור (רישום) משתמש בפורמט I-type שבו השתמשתם בעבר.

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

31..20 19..15 14..12 11.7 6.0
12 5 3 5 7
imm12 rs1 func3 rd קוד פעולה

הפעם לא צריך לבצע שינוי בפורמט.

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

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

הם מקודדים באופן הבא:

31..25 24..20 19..15 14..12 11.7 6..0 שם קוד הפעולה
imm7 rs2 rs1 000 imm5 110 0011 beq
imm7 rs2 rs1 101 imm5 110 0011 bge
imm7 rs2 rs1 111 imm5 110 0011 Bgeu
imm7 rs2 rs1 100 imm5 110 0011 blt
imm7 rs2 rs1 110 imm5 110 0011 bltu
imm7 rs2 rs1 001 imm5 110 0011 bne
func3 קוד פעולה

ההוראה jal מקודדת באופן הבא:

31..12 11.7 6..0 שם opcode
imm20 rd 110 1111 חלול
קוד פעולה

ההוראה jalr מקודדת באופן הבא:

31..20 19..15 14..12 11.7 6.0 opcode_name
imm12 rs1 000 rd 110 0111 Jalr
func3 קוד פעולה

מבצעים את השינויים הרצויים ומבצעים build. בודקים את הפלט שנוצר. בדיוק כמו קודם, אפשר לבדוק את העבודה שלכם מול riscv32i.bin_fmt.


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

הוראות האחסון משתמשות בקידוד מסוג S, שהוא זהה לקידוד מסוג B שמשמש את הוראות ההסתעפות, מלבד ההרכב של הערך המיידי. בחרנו להוסיף את הפורמט SType כדי לשמור על תאימות למסמכי העזרה של RiscV.

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

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

  overlays:
    signed s_imm[12] = imm7, imm5;

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

הוראות החנות מקודדות כך:

31..25 24..20 19..15 14..12 11.7 6..0 שם קוד הפעולה
imm7 rs2 rs1 000 imm5 010 0011 sb
imm7 rs2 rs1 001 imm5 010 0011 sh
imm7 rs2 rs1 010 imm5 010 0011 sw
func3 קוד פעולה

מבצעים את השינויים הרצויים ומבצעים build. בודקים את הפלט שנוצר. בדיוק כמו קודם, אפשר לבדוק את העבודה שלכם מול riscv32i.bin_fmt.


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

הוראות הטעינה משתמשות בפורמט I-Type. אין צורך לבצע שינויים בנתונים האלה.

הקידודים הם:

31..20 19..15 14..12 11.7 6.0 opcode_name
imm12 rs1 000 rd 000 0011 lb
imm12 rs1 100 שלישי 000 0011 lbu
imm12 rs1 001 rd 000 0011 lh
imm12 rs1 101 rd 000 0011 lhu
imm12 rs1 010 rd 000 0011 lw
func3 קוד פעולה

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

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